|
| 1 | +// WhiteboardCanvas.jsx |
| 2 | +import { useRef, useState, useEffect } from "react"; |
| 3 | +import "../Styles/WhiteboardCanvas.css"; |
| 4 | + |
| 5 | +export default function WhiteboardCanvas() { |
| 6 | + const canvasRef = useRef(null); |
| 7 | + const contextRef = useRef(null); |
| 8 | + |
| 9 | + const [isDrawing, setIsDrawing] = useState(false); |
| 10 | + const [lineColor, setLineColor] = useState("#000000"); |
| 11 | + const [lineWidth, setLineWidth] = useState(3); |
| 12 | + |
| 13 | + // 1) NEW: Undo stack state |
| 14 | + const [undoStack, setUndoStack] = useState([]); |
| 15 | + |
| 16 | + // Example color palette |
| 17 | + const colorOptions = [ |
| 18 | + "#000000", "#7F7F7F", "#BFBFBF", "#FFFFFF", |
| 19 | + "#FF0000", "#FF7F00", "#FFFF00", "#7FFF00", |
| 20 | + "#00FF00", "#00FF7F", "#00FFFF", "#007FFF", |
| 21 | + "#0000FF", "#7F00FF", "#FF00FF", "#FF007F" |
| 22 | + ]; |
| 23 | + |
| 24 | + /** |
| 25 | + * Set up the canvas size and context for drawing |
| 26 | + */ |
| 27 | + const resizeCanvas = () => { |
| 28 | + const canvas = canvasRef.current; |
| 29 | + if (!canvas) return; |
| 30 | + |
| 31 | + const scale = window.devicePixelRatio || 1; |
| 32 | + canvas.width = canvas.clientWidth * scale; |
| 33 | + canvas.height = canvas.clientHeight * scale; |
| 34 | + |
| 35 | + const context = canvas.getContext("2d"); |
| 36 | + context.scale(scale, scale); |
| 37 | + context.lineCap = "round"; |
| 38 | + context.lineJoin = "round"; |
| 39 | + |
| 40 | + contextRef.current = context; |
| 41 | + }; |
| 42 | + |
| 43 | + // Resize only on mount or window resize (not on color/width changes) |
| 44 | + useEffect(() => { |
| 45 | + function handleResize() { |
| 46 | + resizeCanvas(); |
| 47 | + } |
| 48 | + // Initial sizing |
| 49 | + resizeCanvas(); |
| 50 | + |
| 51 | + // If you want dynamic resizing |
| 52 | + window.addEventListener("resize", handleResize); |
| 53 | + return () => window.removeEventListener("resize", handleResize); |
| 54 | + }, []); |
| 55 | + |
| 56 | + // Update stroke style on color/width changes |
| 57 | + useEffect(() => { |
| 58 | + if (contextRef.current) { |
| 59 | + contextRef.current.strokeStyle = lineColor; |
| 60 | + contextRef.current.lineWidth = lineWidth; |
| 61 | + } |
| 62 | + }, [lineColor, lineWidth]); |
| 63 | + |
| 64 | + // 2) NEW: After the canvas is set up, capture its initial state for undo |
| 65 | + useEffect(() => { |
| 66 | + if (canvasRef.current && contextRef.current) { |
| 67 | + const canvas = canvasRef.current; |
| 68 | + const ctx = contextRef.current; |
| 69 | + const initialData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| 70 | + setUndoStack([initialData]); |
| 71 | + } |
| 72 | + }, []); |
| 73 | + |
| 74 | + // 3) NEW: Helper function to push the current canvas to undo stack |
| 75 | + const pushToUndoStack = () => { |
| 76 | + if (!canvasRef.current || !contextRef.current) return; |
| 77 | + const canvas = canvasRef.current; |
| 78 | + const data = contextRef.current.getImageData(0, 0, canvas.width, canvas.height); |
| 79 | + setUndoStack((prevStack) => [...prevStack, data]); |
| 80 | + }; |
| 81 | + |
| 82 | + // 4) NEW: Undo logic |
| 83 | + const undoLastAction = () => { |
| 84 | + if (undoStack.length > 1) { |
| 85 | + const newStack = [...undoStack]; |
| 86 | + newStack.pop(); // Remove current state |
| 87 | + const previous = newStack[newStack.length - 1]; |
| 88 | + contextRef.current.putImageData(previous, 0, 0); |
| 89 | + setUndoStack(newStack); |
| 90 | + } else { |
| 91 | + // If nothing left, just clear |
| 92 | + clearCanvas(); |
| 93 | + } |
| 94 | + }; |
| 95 | + |
| 96 | + // Mouse event handlers |
| 97 | + const startDrawing = (e) => { |
| 98 | + const { offsetX, offsetY } = e.nativeEvent; |
| 99 | + contextRef.current.beginPath(); |
| 100 | + contextRef.current.moveTo(offsetX, offsetY); |
| 101 | + setIsDrawing(true); |
| 102 | + }; |
| 103 | + |
| 104 | + const draw = (e) => { |
| 105 | + if (!isDrawing) return; |
| 106 | + const { offsetX, offsetY } = e.nativeEvent; |
| 107 | + contextRef.current.lineTo(offsetX, offsetY); |
| 108 | + contextRef.current.stroke(); |
| 109 | + }; |
| 110 | + |
| 111 | + const stopDrawing = () => { |
| 112 | + if (isDrawing) { |
| 113 | + contextRef.current.closePath(); |
| 114 | + setIsDrawing(false); |
| 115 | + // 5) NEW: After each stroke, capture the final canvas state |
| 116 | + pushToUndoStack(); |
| 117 | + } |
| 118 | + }; |
| 119 | + |
| 120 | + // Utility buttons |
| 121 | + const clearCanvas = () => { |
| 122 | + const canvas = canvasRef.current; |
| 123 | + contextRef.current.clearRect(0, 0, canvas.width, canvas.height); |
| 124 | + }; |
| 125 | + |
| 126 | + const downloadCanvas = () => { |
| 127 | + const canvas = canvasRef.current; |
| 128 | + const link = document.createElement("a"); |
| 129 | + link.download = "lessonconnect_drawing.png"; |
| 130 | + link.href = canvas.toDataURL(); |
| 131 | + link.click(); |
| 132 | + }; |
| 133 | + |
| 134 | + // Placeholder for extra tools |
| 135 | + const handleToolClick = (toolName) => { |
| 136 | + alert(`Tool ${toolName} clicked! (Feature to be added)`); |
| 137 | + }; |
| 138 | + |
| 139 | + // 6) NEW: Listen for Eraser button click => switch to eraser mode |
| 140 | + useEffect(() => { |
| 141 | + const eraserButton = document.querySelector(".tool-btn[title='Eraser']"); |
| 142 | + if (!eraserButton) return; |
| 143 | + |
| 144 | + const handleEraserClick = () => { |
| 145 | + if (contextRef.current) { |
| 146 | + contextRef.current.globalCompositeOperation = "destination-out"; |
| 147 | + // Optionally adjust eraser size: |
| 148 | + setLineWidth(20); |
| 149 | + } |
| 150 | + }; |
| 151 | + |
| 152 | + eraserButton.addEventListener("click", handleEraserClick); |
| 153 | + return () => { |
| 154 | + eraserButton.removeEventListener("click", handleEraserClick); |
| 155 | + }; |
| 156 | + }, []); |
| 157 | + |
| 158 | + // 7) NEW: Listen for Pencil button click => switch back to normal drawing |
| 159 | + useEffect(() => { |
| 160 | + const pencilButton = document.querySelector(".tool-btn[title='Pencil']"); |
| 161 | + if (!pencilButton) return; |
| 162 | + |
| 163 | + const handlePencilClick = () => { |
| 164 | + if (contextRef.current) { |
| 165 | + contextRef.current.globalCompositeOperation = "source-over"; |
| 166 | + // Reset brush size if desired: |
| 167 | + setLineWidth(3); |
| 168 | + } |
| 169 | + }; |
| 170 | + |
| 171 | + pencilButton.addEventListener("click", handlePencilClick); |
| 172 | + return () => { |
| 173 | + pencilButton.removeEventListener("click", handlePencilClick); |
| 174 | + }; |
| 175 | + }, []); |
| 176 | + |
| 177 | + return ( |
| 178 | + // Unique parent class to scope styling: |
| 179 | + <div className="whiteboard-container"> |
| 180 | + <div className="whiteboard-canvas-container"> |
| 181 | + {/* Top bar */} |
| 182 | + <div className="whiteboard-topbar glass-card"> |
| 183 | + <div className="whiteboard-title">Whiteboard</div> |
| 184 | + <div className="whiteboard-tools"> |
| 185 | + <button |
| 186 | + className="tool-btn neon-hover" |
| 187 | + title="Cursor" |
| 188 | + onClick={() => handleToolClick("Cursor")} |
| 189 | + > |
| 190 | + <i className="fas fa-mouse-pointer"></i> |
| 191 | + </button> |
| 192 | + <button |
| 193 | + className="tool-btn neon-hover" |
| 194 | + title="Pencil" |
| 195 | + onClick={() => handleToolClick("Pencil")} |
| 196 | + > |
| 197 | + <i className="fas fa-pencil-alt"></i> |
| 198 | + </button> |
| 199 | + <button |
| 200 | + className="tool-btn neon-hover" |
| 201 | + title="Rectangle" |
| 202 | + onClick={() => handleToolClick("Rectangle")} |
| 203 | + > |
| 204 | + <i className="far fa-square"></i> |
| 205 | + </button> |
| 206 | + <button |
| 207 | + className="tool-btn neon-hover" |
| 208 | + title="Circle" |
| 209 | + onClick={() => handleToolClick("Circle")} |
| 210 | + > |
| 211 | + <i className="far fa-circle"></i> |
| 212 | + </button> |
| 213 | + <button |
| 214 | + className="tool-btn neon-hover" |
| 215 | + title="Eraser" |
| 216 | + onClick={() => handleToolClick("Eraser")} |
| 217 | + > |
| 218 | + <i className="fas fa-eraser"></i> |
| 219 | + </button> |
| 220 | + </div> |
| 221 | + </div> |
| 222 | + |
| 223 | + {/* Secondary toolbar */} |
| 224 | + <div className="toolbar glass-card"> |
| 225 | + <label className="color-label">Color:</label> |
| 226 | + {colorOptions.map((color) => ( |
| 227 | + <button |
| 228 | + key={color} |
| 229 | + className="color-button" |
| 230 | + style={{ backgroundColor: color }} |
| 231 | + onClick={() => setLineColor(color)} |
| 232 | + /> |
| 233 | + ))} |
| 234 | + <label className="width-label"> |
| 235 | + Brush: |
| 236 | + <input |
| 237 | + type="range" |
| 238 | + min="1" |
| 239 | + max="20" |
| 240 | + value={lineWidth} |
| 241 | + onChange={(e) => setLineWidth(e.target.value)} |
| 242 | + /> |
| 243 | + </label> |
| 244 | + <button className="action-button neon-hover" onClick={clearCanvas}> |
| 245 | + Clear |
| 246 | + </button> |
| 247 | + |
| 248 | + {/* 8) NEW: Undo button */} |
| 249 | + <button className="action-button neon-hover" onClick={undoLastAction}> |
| 250 | + Undo |
| 251 | + </button> |
| 252 | + |
| 253 | + <button className="action-button neon-hover" onClick={downloadCanvas}> |
| 254 | + Download |
| 255 | + </button> |
| 256 | + </div> |
| 257 | + |
| 258 | + {/* Canvas */} |
| 259 | + <div |
| 260 | + className="canvas-container glass-card" |
| 261 | + onMouseDown={startDrawing} |
| 262 | + onMouseMove={draw} |
| 263 | + onMouseUp={stopDrawing} |
| 264 | + onMouseLeave={stopDrawing} |
| 265 | + > |
| 266 | + <canvas ref={canvasRef} className="drawing-canvas" /> |
| 267 | + </div> |
| 268 | + </div> |
| 269 | + </div> |
| 270 | + ); |
| 271 | +} |
0 commit comments