main
parent
99d0108652
commit
be9a045b4a
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue