drawing-web-app-react/src/components/DrawingCanvas.js

241 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useRef, useState, useEffect } 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();
};
const drawShape = (ctx, shape) => {
ctx.beginPath();
ctx.strokeStyle = shape.color || '#000000';
ctx.lineWidth = shape.thickness || 2;
ctx.lineCap = 'round';
if (shape.type === 'rectangle') {
ctx.rect(shape.x || 0, shape.y || 0, shape.width || 0, shape.height || 0);
} else if (shape.type === 'circle') {
ctx.arc(shape.x || 0, shape.y || 0, shape.radius || 0, 0, Math.PI * 2);
}
ctx.stroke();
};
const DrawingCanvas = () => {
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
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([]);
useEffect(() => {
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);
}
});
// 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]);
const handleMouseDown = (e) => {
setIsDrawing(true);
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (currentTool === 'pencil') {
setLastPosition({ x, y });
} else if (currentTool === 'rectangle') {
setShapes([...shapes, {
type: 'rectangle',
x,
y,
width: 0,
height: 0,
color,
thickness
}]);
} else if (currentTool === 'circle') {
setShapes([...shapes, {
type: 'circle',
x,
y,
radius: 0,
color,
thickness
}]);
}
};
const handleMouseMove = (e) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
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;
});
// 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 = () => {
setIsDrawing(false);
setLastPosition(null);
setUndoHistory([]);
};
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));
}
}
};
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]);
}
}
};
return (
<div className="drawing-container">
<div className="toolbar">
<button onClick={() => setCurrentTool('pencil')} className={`tool-button ${currentTool === 'pencil' ? 'active' : ''}`}>
</button>
<button onClick={() => setCurrentTool('rectangle')} className={`tool-button ${currentTool === 'rectangle' ? 'active' : ''}`}>
🟨
</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">
<ChromePicker color={color} onChangeComplete={(color) => setColor(color.hex)} />
</div>
<div className="thickness-controls">
<button onClick={() => setThickness(thickness - 1)} disabled={thickness <= 1}></button>
<span>{thickness}</span>
<button onClick={() => setThickness(thickness + 1)}></button>
</div>
</div>
<div className="canvas-container">
<canvas
ref={canvasRef}
className="drawing-canvas"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
/>
</div>
</div>
);
};
export default DrawingCanvas;