diff --git a/.gitignore b/.gitignore index 3c3629e..08b2553 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -node_modules +node_modules diff --git a/src/components/DrawingCanvas.js b/src/components/DrawingCanvas.js index 9e8dcd5..3efce3d 100644 --- a/src/components/DrawingCanvas.js +++ b/src/components/DrawingCanvas.js @@ -1,236 +1,213 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ChromePicker } from 'react-color'; -// Drawing utilities -const drawLine = (ctx, x1, y1, x2, y2, color, thickness) => { - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.strokeStyle = color || '#000000'; - ctx.lineWidth = thickness || 2; - ctx.lineCap = 'round'; - ctx.stroke(); -}; - +// Update drawShape to handle pencil strokes const drawShape = (ctx, shape) => { ctx.beginPath(); - ctx.strokeStyle = shape.color || '#000000'; - ctx.lineWidth = shape.thickness || 2; - ctx.lineCap = 'round'; - + ctx.strokeStyle = shape.color; + ctx.lineWidth = shape.thickness; if (shape.type === 'rectangle') { - ctx.rect(shape.x || 0, shape.y || 0, shape.width || 0, shape.height || 0); + ctx.rect(shape.x, shape.y, shape.width, shape.height); + ctx.stroke(); } else if (shape.type === 'circle') { - ctx.arc(shape.x || 0, shape.y || 0, shape.radius || 0, 0, Math.PI * 2); + ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2); + ctx.stroke(); + } else if (shape.type === 'pencil') { + if (shape.points.length < 2) return; + ctx.moveTo(shape.points[0].x, shape.points[0].y); + for (let i = 1; i < shape.points.length; i++) { + ctx.lineTo(shape.points[i].x, shape.points[i].y); + } + ctx.stroke(); } - - ctx.stroke(); }; const DrawingCanvas = () => { const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); + const [lastPosition, setLastPosition] = useState(null); + const [history, setHistory] = useState([]); + const [undoHistory, setUndoHistory] = useState([]); + const [shapes, setShapes] = useState([]); const [currentTool, setCurrentTool] = useState('pencil'); const [color, setColor] = useState('#000000'); const [thickness, setThickness] = useState(2); - const [history, setHistory] = useState([]); - const [shapes, setShapes] = useState([]); - const [lastPosition, setLastPosition] = useState(null); - const [undoHistory, setUndoHistory] = useState([]); + // Resize canvas on window resize useEffect(() => { + const resizeCanvas = () => { + const canvas = canvasRef.current; + if (!canvas) return; + canvas.width = window.innerWidth - 300; + canvas.height = window.innerHeight - 100; + redraw(); + }; + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + return () => window.removeEventListener('resize', resizeCanvas); + // eslint-disable-next-line + }, []); + + // Redraw everything + const redraw = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set initial canvas size - canvas.width = window.innerWidth - 300; - canvas.height = window.innerHeight - 100; - - // Clear canvas first ctx.clearRect(0, 0, canvas.width, canvas.height); - - // Draw all shapes - shapes.forEach((shape) => { - drawShape(ctx, shape); - }); - - // Draw all pencil strokes from history - history.forEach((action) => { - if (action.type === 'draw') { - drawLine(ctx, action.x1, action.y1, action.x2, action.y2, action.color, action.thickness); + shapes.forEach(shape => drawShape(ctx, shape)); + history.forEach(action => { + if (action.type === 'stroke') { + ctx.putImageData(action.imageData, 0, 0); } }); + }, [shapes, history]); - // Force a re-render to ensure everything is drawn - if (canvas) { - canvas.style.display = 'none'; - // Trigger reflow by forcing layout calculation - const width = canvas.offsetWidth; - canvas.style.display = 'block'; - } - }, [history, shapes]); + useEffect(() => { + redraw(); + }, [shapes, history, redraw]); - const handleMouseDown = (e) => { - setIsDrawing(true); + // Get mouse or touch position + const getPos = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + if (e.touches) { + return { + x: e.touches[0].clientX - rect.left, + y: e.touches[0].clientY - rect.top, + }; + } + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }; + }; + + const handlePointerDown = (e) => { + e.preventDefault(); + const { x, y } = getPos(e); + setIsDrawing(true); if (currentTool === 'pencil') { - setLastPosition({ x, y }); - } else if (currentTool === 'rectangle') { - setShapes([...shapes, { - type: 'rectangle', + // Start a new pencil stroke + setShapes(prev => [ + ...prev, + { + type: 'pencil', + color, + thickness, + points: [{ x, y }] + } + ]); + } else if (currentTool === 'rectangle' || currentTool === 'circle') { + const newShape = { + type: currentTool, x, y, width: 0, height: 0, - color, - thickness - }]); - } else if (currentTool === 'circle') { - setShapes([...shapes, { - type: 'circle', - x, - y, radius: 0, color, - thickness - }]); + thickness, + }; + setShapes(prev => [...prev, newShape]); } }; - const handleMouseMove = (e) => { + const handlePointerMove = (e) => { if (!isDrawing) return; - - const canvas = canvasRef.current; - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const { x, y } = getPos(e); if (currentTool === 'pencil') { - const lastPos = lastPosition; - - if (lastPos) { - // Don't draw immediately, just update history - setHistory(prevHistory => [...prevHistory, { - type: 'draw', - x1: lastPos.x, - y1: lastPos.y, - x2: x, - y2: y, - color, - thickness - }]); - } - - // Update last position - setLastPosition({ x, y }); - } else if (currentTool === 'rectangle' || currentTool === 'circle') { - const updatedShapes = shapes.map((shape, index) => { - if (index === shapes.length - 1) { - // Update the shape dimensions - if (currentTool === 'rectangle') { - const width = Math.abs(x - shape.x); - const height = Math.abs(y - shape.y); - return { ...shape, width, height }; - } else if (currentTool === 'circle') { - const radius = Math.sqrt(Math.pow(x - shape.x, 2) + Math.pow(y - shape.y, 2)); - return { ...shape, radius }; - } - } - return shape; + setShapes(prev => { + if (prev.length === 0) return prev; + const updated = [...prev]; + const lastStroke = { ...updated[updated.length - 1] }; + lastStroke.points = [...lastStroke.points, { x, y }]; + updated[updated.length - 1] = lastStroke; + return updated; + }); + } else if (currentTool === 'rectangle' || currentTool === 'circle') { + setShapes(prev => { + if (prev.length === 0) return prev; + const updated = [...prev]; + const shape = { ...updated[updated.length - 1] }; + if (currentTool === 'rectangle') { + shape.width = x - shape.x; + shape.height = y - shape.y; + } else if (currentTool === 'circle') { + shape.radius = Math.sqrt(Math.pow(x - shape.x, 2) + Math.pow(y - shape.y, 2)); + } + updated[updated.length - 1] = shape; + return updated; }); - - // Add shape to history when it's created - if (updatedShapes.length > 0) { - const lastShape = updatedShapes[updatedShapes.length - 1]; - setHistory(prevHistory => [...prevHistory, { - type: 'shape', - shape: lastShape - }]); - } - - setShapes(updatedShapes); } }; - const handleMouseUp = () => { + const handlePointerUp = () => { setIsDrawing(false); setLastPosition(null); + // Save a snapshot of shapes for undo/redo + setHistory(prev => [...prev, shapes]); setUndoHistory([]); }; + // Undo: restore the previous snapshot const handleUndo = () => { - if (history.length > 0) { - // Clear the last action from history - setHistory(prevHistory => prevHistory.slice(0, -1)); - - // Add it to undo history - setUndoHistory(prev => [...prev, history[history.length - 1]]); - - // Clear the last shape if it exists - if (shapes.length > 0) { - setShapes(prevShapes => prevShapes.slice(0, -1)); - } - } + if (history.length === 0) return; + setUndoHistory(prev => [...prev, shapes]); + const prevShapes = history[history.length - 1]; + setShapes(prevShapes); + setHistory(prev => prev.slice(0, -1)); }; + // Redo: restore the next snapshot const handleRedo = () => { - if (undoHistory.length > 0) { - const lastUndo = undoHistory[undoHistory.length - 1]; - - // Add the action back to history - setHistory(prevHistory => [...prevHistory, lastUndo]); - - // Remove it from undo history - setUndoHistory(prev => prev.slice(0, -1)); - - // Add the shape back if it exists - if (lastUndo.type === 'shape') { - setShapes(prevShapes => [...prevShapes, lastUndo.shape]); - } - } + if (undoHistory.length === 0) return; + setHistory(prev => [...prev, shapes]); + const nextShapes = undoHistory[undoHistory.length - 1]; + setShapes(nextShapes); + setUndoHistory(prev => prev.slice(0, -1)); }; + // Touch events for mobile + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + canvas.addEventListener('touchstart', handlePointerDown, { passive: false }); + canvas.addEventListener('touchmove', handlePointerMove, { passive: false }); + canvas.addEventListener('touchend', handlePointerUp, { passive: false }); + return () => { + canvas.removeEventListener('touchstart', handlePointerDown); + canvas.removeEventListener('touchmove', handlePointerMove); + canvas.removeEventListener('touchend', handlePointerUp); + }; + // eslint-disable-next-line + }, [isDrawing, lastPosition, currentTool, color, thickness, shapes]); + return (