Kar 2025-07-08 13:41:39 +05:30
parent 99d0108652
commit be9a045b4a
2 changed files with 142 additions and 165 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
node_modules node_modules

View File

@ -1,236 +1,213 @@
import React, { useRef, useState, useEffect } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { ChromePicker } from 'react-color'; import { ChromePicker } from 'react-color';
// Drawing utilities // Update drawShape to handle pencil strokes
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();
};
const drawShape = (ctx, shape) => { const drawShape = (ctx, shape) => {
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = shape.color || '#000000'; ctx.strokeStyle = shape.color;
ctx.lineWidth = shape.thickness || 2; ctx.lineWidth = shape.thickness;
ctx.lineCap = 'round';
if (shape.type === 'rectangle') { 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') { } 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 DrawingCanvas = () => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false); 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 [currentTool, setCurrentTool] = useState('pencil');
const [color, setColor] = useState('#000000'); const [color, setColor] = useState('#000000');
const [thickness, setThickness] = useState(2); 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(() => { 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; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); 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); ctx.clearRect(0, 0, canvas.width, canvas.height);
shapes.forEach(shape => drawShape(ctx, shape));
// Draw all shapes history.forEach(action => {
shapes.forEach((shape) => { if (action.type === 'stroke') {
drawShape(ctx, shape); ctx.putImageData(action.imageData, 0, 0);
});
// 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, history]);
// Force a re-render to ensure everything is drawn useEffect(() => {
if (canvas) { redraw();
canvas.style.display = 'none'; }, [shapes, history, redraw]);
// Trigger reflow by forcing layout calculation
const width = canvas.offsetWidth;
canvas.style.display = 'block';
}
}, [history, shapes]);
const handleMouseDown = (e) => { // Get mouse or touch position
setIsDrawing(true); const getPos = (e) => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left; if (e.touches) {
const y = e.clientY - rect.top; 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') { if (currentTool === 'pencil') {
setLastPosition({ x, y }); // Start a new pencil stroke
} else if (currentTool === 'rectangle') { setShapes(prev => [
setShapes([...shapes, { ...prev,
type: 'rectangle', {
type: 'pencil',
color,
thickness,
points: [{ x, y }]
}
]);
} else if (currentTool === 'rectangle' || currentTool === 'circle') {
const newShape = {
type: currentTool,
x, x,
y, y,
width: 0, width: 0,
height: 0, height: 0,
color,
thickness
}]);
} else if (currentTool === 'circle') {
setShapes([...shapes, {
type: 'circle',
x,
y,
radius: 0, radius: 0,
color, color,
thickness thickness,
}]); };
setShapes(prev => [...prev, newShape]);
} }
}; };
const handleMouseMove = (e) => { const handlePointerMove = (e) => {
if (!isDrawing) return; if (!isDrawing) return;
const { x, y } = getPos(e);
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (currentTool === 'pencil') { if (currentTool === 'pencil') {
const lastPos = lastPosition; setShapes(prev => {
if (prev.length === 0) return prev;
if (lastPos) { const updated = [...prev];
// Don't draw immediately, just update history const lastStroke = { ...updated[updated.length - 1] };
setHistory(prevHistory => [...prevHistory, { lastStroke.points = [...lastStroke.points, { x, y }];
type: 'draw', updated[updated.length - 1] = lastStroke;
x1: lastPos.x, return updated;
y1: lastPos.y, });
x2: x, } else if (currentTool === 'rectangle' || currentTool === 'circle') {
y2: y, setShapes(prev => {
color, if (prev.length === 0) return prev;
thickness const updated = [...prev];
}]); const shape = { ...updated[updated.length - 1] };
} if (currentTool === 'rectangle') {
shape.width = x - shape.x;
// Update last position shape.height = y - shape.y;
setLastPosition({ x, y }); } else if (currentTool === 'circle') {
} else if (currentTool === 'rectangle' || currentTool === 'circle') { shape.radius = Math.sqrt(Math.pow(x - shape.x, 2) + Math.pow(y - shape.y, 2));
const updatedShapes = shapes.map((shape, index) => { }
if (index === shapes.length - 1) { updated[updated.length - 1] = shape;
// Update the shape dimensions return updated;
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;
}); });
// 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); setIsDrawing(false);
setLastPosition(null); setLastPosition(null);
// Save a snapshot of shapes for undo/redo
setHistory(prev => [...prev, shapes]);
setUndoHistory([]); setUndoHistory([]);
}; };
// Undo: restore the previous snapshot
const handleUndo = () => { const handleUndo = () => {
if (history.length > 0) { if (history.length === 0) return;
// Clear the last action from history setUndoHistory(prev => [...prev, shapes]);
setHistory(prevHistory => prevHistory.slice(0, -1)); const prevShapes = history[history.length - 1];
setShapes(prevShapes);
// Add it to undo history setHistory(prev => prev.slice(0, -1));
setUndoHistory(prev => [...prev, history[history.length - 1]]);
// Clear the last shape if it exists
if (shapes.length > 0) {
setShapes(prevShapes => prevShapes.slice(0, -1));
}
}
}; };
// Redo: restore the next snapshot
const handleRedo = () => { const handleRedo = () => {
if (undoHistory.length > 0) { if (undoHistory.length === 0) return;
const lastUndo = undoHistory[undoHistory.length - 1]; setHistory(prev => [...prev, shapes]);
const nextShapes = undoHistory[undoHistory.length - 1];
// Add the action back to history setShapes(nextShapes);
setHistory(prevHistory => [...prevHistory, lastUndo]); setUndoHistory(prev => prev.slice(0, -1));
// 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]);
}
}
}; };
// 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 ( return (
<div className="drawing-container"> <div className="drawing-container">
<div className="toolbar"> <div className="toolbar">
<button onClick={() => setCurrentTool('pencil')} className={`tool-button ${currentTool === 'pencil' ? 'active' : ''}`}> <button onClick={() => setCurrentTool('pencil')} className={`tool-button ${currentTool === 'pencil' ? 'active' : ''}`}></button>
<button onClick={() => setCurrentTool('rectangle')} className={`tool-button ${currentTool === 'rectangle' ? 'active' : ''}`}>🟨</button>
</button> <button onClick={() => setCurrentTool('circle')} className={`tool-button ${currentTool === 'circle' ? 'active' : ''}`}></button>
<button onClick={() => setCurrentTool('rectangle')} className={`tool-button ${currentTool === 'rectangle' ? 'active' : ''}`}> <button onClick={handleUndo} className="tool-button" disabled={history.length === 0}></button>
🟨 <button onClick={handleRedo} className="tool-button" disabled={undoHistory.length === 0}></button>
</button>
<button onClick={() => setCurrentTool('circle')} className={`tool-button ${currentTool === 'circle' ? 'active' : ''}`}>
</button>
<button onClick={handleUndo} className="tool-button" disabled={history.length === 0}>
</button>
<button onClick={handleRedo} className="tool-button" disabled={undoHistory.length === 0}>
</button>
<div className="color-picker"> <div className="color-picker">
<ChromePicker color={color} onChangeComplete={(color) => setColor(color.hex)} /> <ChromePicker color={color} onChangeComplete={(c) => setColor(c.hex)} />
</div> </div>
<div className="thickness-controls"> <div className="thickness-controls">
<button onClick={() => setThickness(thickness - 1)} disabled={thickness <= 1}></button> <button onClick={() => setThickness(t => Math.max(1, t - 1))} disabled={thickness <= 1}></button>
<span>{thickness}</span> <span>{thickness}</span>
<button onClick={() => setThickness(thickness + 1)}></button> <button onClick={() => setThickness(t => t + 1)}></button>
</div> </div>
</div> </div>
<div className="canvas-container"> <div className="canvas-container">
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="drawing-canvas" className="drawing-canvas"
onMouseDown={handleMouseDown} onMouseDown={handlePointerDown}
onMouseMove={handleMouseMove} onMouseMove={handlePointerMove}
onMouseUp={handleMouseUp} onMouseUp={handlePointerUp}
/> />
</div> </div>
</div> </div>