| import React from 'react';
|
| import {
|
| Undo2,
|
| Redo2,
|
| Pencil,
|
| Hand,
|
| Square,
|
| Eraser,
|
| Sun,
|
| Moon,
|
| ZoomIn,
|
| ZoomOut,
|
| Grid,
|
| GripHorizontal,
|
| RotateCcw,
|
| Scan,
|
| Download,
|
| Menu,
|
| X,
|
| Ruler,
|
| Palette
|
| } from 'lucide-react';
|
|
|
| const PRESET_COLORS = [
|
| '#000000', '#FFFFFF', '#FF4500', '#FFA500',
|
| '#FF0000', '#FF1493', '#800080', '#4B0082',
|
| '#008000', '#C0C0C0', '#FFA500', '#4169E1',
|
| '#00FF00', '#40E0D0', '#0000FF', '#FFD700'
|
| ];
|
|
|
|
|
|
|
|
|
| const DrawingBoard = ({
|
| tool,
|
| setTool,
|
| elements,
|
| pencilSize,
|
| setPencilSize,
|
| scale,
|
| setScale,
|
| onZoom,
|
| undo,
|
| redo,
|
| action,
|
| selectedElement,
|
| panOffset,
|
| scaleOffset,
|
| handleMouseDown,
|
| handleMouseMove,
|
| handleMouseUp,
|
| handleBlur,
|
| captureDrawnArea,
|
| handleDetectRegions,
|
| handleDownloadRegions
|
| }) => {
|
| const textAreaRef = React.useRef(null);
|
| const backgroundCanvasRef = React.useRef(null);
|
| const drawingCanvasRef = React.useRef(null);
|
| const [backgroundType, setBackgroundType] = React.useState('none');
|
| const [gridSize, setGridSize] = React.useState(45);
|
| const [isMenuOpen, setIsMenuOpen] = React.useState(false);
|
| const [showDownloadButton, setShowDownloadButton] = React.useState(false);
|
| const [showColorPicker, setShowColorPicker] = React.useState(false);
|
| const [currentColor, setCurrentColor] = React.useState('#000000');
|
| const [customColor, setCustomColor] = React.useState('#000000');
|
| const [darkMode, setDarkMode] = React.useState(() => {
|
| const savedMode = localStorage.getItem('drawingBoardDarkMode');
|
| if (savedMode !== null) {
|
| return savedMode === 'true';
|
| }
|
| return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| });
|
|
|
| React.useEffect(() => {
|
| const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
| const handleChange = (e) => {
|
| const savedMode = localStorage.getItem('drawingBoardDarkMode');
|
| if (savedMode === null) {
|
| setDarkMode(e.matches);
|
| }
|
| };
|
|
|
| mediaQuery.addEventListener('change', handleChange);
|
| return () => mediaQuery.removeEventListener('change', handleChange);
|
| }, []);
|
|
|
| React.useEffect(() => {
|
| localStorage.setItem('drawingBoardDarkMode', darkMode);
|
| drawElements();
|
| }, [darkMode]);
|
|
|
| const drawBackground = React.useCallback(() => {
|
| const canvas = backgroundCanvasRef.current;
|
| if (!canvas) return;
|
|
|
| const ctx = canvas.getContext('2d');
|
| const width = canvas.width;
|
| const height = canvas.height;
|
|
|
| ctx.fillStyle = darkMode ? '#ffffff' : '#000000';
|
| ctx.fillRect(0, 0, width, height);
|
|
|
| const offsetX = (panOffset.x * scale - scaleOffset.x) % (gridSize * scale);
|
| const offsetY = (panOffset.y * scale - scaleOffset.y) % (gridSize * scale);
|
| const gridColor = darkMode ? 'rgba(0, 0, 0, 0.6)' : 'rgba(255, 255, 255, 0.6)';
|
|
|
| if (backgroundType === 'grid') {
|
| const majorGridSize = gridSize *5;
|
| const majorGridColor = darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
| const minorGridColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
| ctx.beginPath();
|
| ctx.strokeStyle = gridColor;
|
| ctx.lineWidth = 0.5;
|
|
|
| for (let x = offsetX; x < width; x += gridSize * scale) {
|
| ctx.moveTo(x, 0);
|
| ctx.lineTo(x, height);
|
| }
|
|
|
| for (let y = offsetY; y < height; y += gridSize * scale) {
|
| ctx.moveTo(0, y);
|
| ctx.lineTo(width, y);
|
| }
|
|
|
| ctx.stroke();
|
| ctx.beginPath();
|
| ctx.strokeStyle = majorGridColor;
|
| ctx.lineWidth = 1.5;
|
|
|
|
|
| const majorOffsetX = offsetX - (offsetX % (majorGridSize * scale));
|
| const majorOffsetY = offsetY - (offsetY % (majorGridSize * scale));
|
| for (let x = majorOffsetX; x < width; x += majorGridSize * scale) {
|
| ctx.moveTo(x, 0);
|
| ctx.lineTo(x, height);
|
| }
|
|
|
|
|
| for (let y = majorOffsetY; y < height; y += majorGridSize * scale) {
|
| ctx.moveTo(0, y);
|
| ctx.lineTo(width, y);
|
| }
|
| ctx.stroke();
|
| } else if (backgroundType === 'dots') {
|
| ctx.fillStyle = gridColor;
|
| const dotSize = 2;
|
|
|
| for (let x = offsetX; x < width; x += gridSize * scale) {
|
| for (let y = offsetY; y < height; y += gridSize * scale) {
|
| ctx.beginPath();
|
| ctx.arc(x, y, 1, 0, Math.PI * 2);
|
| ctx.fill();
|
| }
|
| }
|
| }
|
| }, [backgroundType, gridSize, scale, panOffset, scaleOffset, darkMode]);
|
| const toggleBackground = () => {
|
| setBackgroundType(prev => {
|
| if (prev === 'none') return 'grid';
|
| if (prev === 'grid') return 'dots';
|
| return 'none';
|
| });
|
| };
|
| const drawElements = React.useCallback(() => {
|
| const canvas = drawingCanvasRef.current;
|
| if (!canvas) return;
|
|
|
| const ctx = canvas.getContext('2d');
|
| ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
| ctx.save();
|
| ctx.translate(panOffset.x * scale - scaleOffset.x, panOffset.y * scale - scaleOffset.y);
|
| ctx.scale(scale, scale);
|
|
|
| elements.forEach(element => {
|
| if (element.type === 'pencil') {
|
| ctx.beginPath();
|
| ctx.strokeStyle = element.color || currentColor;
|
| ctx.lineWidth = element.size;
|
| ctx.lineCap = 'round';
|
| ctx.lineJoin = 'round';
|
|
|
| element.points.forEach((point, index) => {
|
| if (index === 0) {
|
| ctx.moveTo(point.x, point.y);
|
| } else {
|
| ctx.lineTo(point.x, point.y);
|
| }
|
| });
|
| ctx.stroke();
|
| } else if (element.type === 'line') {
|
| ctx.beginPath();
|
| ctx.strokeStyle = element.color || currentColor;
|
| ctx.lineWidth = 2;
|
| ctx.moveTo(element.x1, element.y1);
|
| ctx.lineTo(element.x2, element.y2);
|
| ctx.stroke();
|
| } else if (element.type === 'rectangle') {
|
| ctx.strokeStyle = element.color || currentColor;
|
| ctx.lineWidth = 2;
|
| ctx.strokeRect(
|
| element.x1,
|
| element.y1,
|
| element.x2 - element.x1,
|
| element.y2 - element.y1
|
| );
|
| } else if (element.type === 'text') {
|
| ctx.font = '24px sans-serif';
|
| ctx.fillStyle = element.color || currentColor;
|
| ctx.fillText(element.text, element.x1, element.y1);
|
| }
|
| });
|
|
|
| ctx.restore();
|
| }, [elements, scale, panOffset, scaleOffset, currentColor]);
|
| React.useEffect(() => {
|
| drawBackground();
|
| drawElements();
|
| }, [drawBackground, drawElements]);
|
| const ColorPickerButton = () => (
|
| <div className="relative">
|
| <button
|
| onClick={() => setShowColorPicker(!showColorPicker)}
|
| className="p-2 rounded hover:bg-opacity-80 flex items-center gap-1"
|
| title="Color Picker"
|
| >
|
| <div
|
| className="w-5 h-5 rounded-full border border-gray-300"
|
| style={{ backgroundColor: currentColor }}
|
| />
|
| <Palette size={16} color={iconColor} />
|
| </button>
|
| {showColorPicker && <ColorPicker />}
|
| </div>
|
| );
|
|
|
| React.useEffect(() => {
|
| const handleResize = () => {
|
| if (backgroundCanvasRef.current && drawingCanvasRef.current) {
|
| backgroundCanvasRef.current.width = window.innerWidth;
|
| backgroundCanvasRef.current.height = window.innerHeight;
|
| drawingCanvasRef.current.width = window.innerWidth;
|
| drawingCanvasRef.current.height = window.innerHeight;
|
| drawBackground();
|
| drawElements();
|
| }
|
| };
|
|
|
| window.addEventListener('resize', handleResize);
|
| handleResize();
|
| return () => window.removeEventListener('resize', handleResize);
|
| }, [drawBackground, drawElements]);
|
|
|
| const toggleDarkMode = () => {
|
| setDarkMode(prev => !prev);
|
| };
|
|
|
| const handleDetectAndShowDownload = () => {
|
| handleDetectRegions();
|
| setShowDownloadButton(true);
|
| setIsMenuOpen(false);
|
| };
|
|
|
| const iconColor = darkMode ? "#000000" : "#ffffff";
|
| const barBgColor = darkMode ? "bg-white" : "bg-gray-800";
|
| const textColor = darkMode ? 'text-black' : 'text-white';
|
| const handleColorChange =(color)=>{
|
| setCurrentColor(color);
|
| setShowColorPicker(false);
|
| };
|
| const handleCustomColorChange = (e) =>{
|
| const newColor = e.target.value;
|
| setCustomColor(newColor);
|
| setCurrentColor(newColor);
|
| };
|
| const ColorPicker = () => (
|
| <div className={`absolute top-full mt-2 p-2 ${barBgColor} rounded-lg shadow-lg`}>
|
| <div className="grid grid-cols-4 gap-2 mb-2">
|
| {PRESET_COLORS.map((color, index) => (
|
| <button
|
| key={index}
|
| onClick={() => handleColorChange(color)}
|
| className="w-6 h-6 rounded-full border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
| style={{
|
| backgroundColor: color,
|
| border: currentColor === color ? '2px solid #3B82F6' : '1px solid #D1D5DB'
|
| }}
|
| />
|
| ))}
|
| </div>
|
| <div className="flex items-center gap-2 mt-2 pt-2 border-t border-gray-600">
|
| <input
|
| type="color"
|
| value={customColor}
|
| onChange={handleCustomColorChange}
|
| className="w-8 h-8 p-0 border-0 rounded cursor-pointer"
|
| />
|
| <input
|
| type="text"
|
| value={customColor.toUpperCase()}
|
| onChange={(e) => {
|
| const val = e.target.value;
|
| if (/^#[0-9A-F]{0,6}$/i.test(val)) {
|
| setCustomColor(val);
|
| if (val.length === 7) setCurrentColor(val);
|
| }
|
| }}
|
| className={`w-20 px-1 py-0.5 text-sm rounded ${
|
| darkMode ? 'bg-gray-700 text-white' : 'bg-white text-black'
|
| }`}
|
| placeholder="#000000"
|
| />
|
| </div>
|
| </div>
|
| );
|
| return (
|
| <div className={`relative w-full h-screen ${darkMode ? 'bg-gray-900' : 'bg-white'}`}>
|
| {/* Corner Menu Button */}
|
| <button
|
| onClick={() => setIsMenuOpen(!isMenuOpen)}
|
| className={`fixed top-4 left-4 z-20 p-2 ${barBgColor} rounded-lg shadow-lg hover:bg-opacity-80`}
|
| >
|
| {isMenuOpen ?
|
| <X size={24} color={iconColor} /> :
|
| <Menu size={24} color={iconColor} />
|
| }
|
| </button>
|
|
|
| {/* Corner Menu Panel */}
|
| {isMenuOpen && (
|
| <div className={`fixed top-16 left-4 z-20 ${barBgColor} rounded-lg shadow-lg p-4 w-64`}>
|
| <div className="flex flex-col gap-4">
|
| {/* Theme Toggle */}
|
| <button
|
| onClick={toggleDarkMode}
|
| className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
|
| >
|
| <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
|
| {darkMode ? 'Light Mode' : 'Dark Mode'}
|
| </span>
|
| {darkMode ?
|
| <Sun size={20} color={iconColor} /> :
|
| <Moon size={20} color={iconColor} />
|
| }
|
| </button>
|
|
|
| {/* Grid Toggle */}
|
| <button
|
| onClick={() => setBackgroundType(prev => prev === 'grid' ? 'none' : 'grid')}
|
| className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
|
| >
|
| <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
|
| Show Grid
|
| </span>
|
| <Grid
|
| size={20}
|
| color={iconColor}
|
| className={backgroundType === 'grid' ? 'opacity-100' : 'opacity-50'}
|
| />
|
| </button>
|
| <button
|
| onClick={toggleBackground}
|
| className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${
|
| darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'
|
| }`}
|
| >
|
| <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
|
| {backgroundType === 'none' ? 'No Grid' :
|
| backgroundType === 'grid' ? 'Grid Lines' : 'Grid Dots'}
|
| </span>
|
| <Grid
|
| size={20}
|
| color={iconColor}
|
| className={backgroundType !== 'none' ? 'opacity-100' : 'opacity-50'}
|
| />
|
| </button>
|
|
|
| {backgroundType !== 'none' && (
|
| <div className="flex flex-col gap-2 p-2">
|
| <div className="flex items-center justify-between">
|
| <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
|
| Grid Size
|
| </span>
|
| <span className={`text-sm ${darkMode ? 'text-black' : 'text-white'}`}>
|
| {gridSize}px
|
| </span>
|
| </div>
|
| <input
|
| type="range"
|
| min="10"
|
| max="100"
|
| value={gridSize}
|
| onChange={(e) => setGridSize(parseInt(e.target.value))}
|
| className="w-full h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer"
|
| />
|
| </div>
|
| )}
|
| <style jsx global>{`
|
| input[type="range"] {
|
| -webkit-appearance: none;
|
| height: 4px;
|
| background: ${darkMode ? '#ffffff40' : '#00000040'};
|
| border-radius: 2px;
|
| outline: none;
|
| }
|
|
|
| input[type="range"]::-webkit-slider-thumb {
|
| -webkit-appearance: none;
|
| appearance: none;
|
| width: 12px;
|
| height: 12px;
|
| background: ${darkMode ? '#ffffff' : '#000000'};
|
| border-radius: 50%;
|
| cursor: pointer;
|
| }
|
|
|
| input[type="range"]::-moz-range-thumb {
|
| width: 12px;
|
| height: 12px;
|
| background: ${darkMode ? '#ffffff' : '#000000'};
|
| border-radius: 50%;
|
| cursor: pointer;
|
| border: none;
|
| }
|
|
|
| input[type="range"]:hover::-webkit-slider-thumb {
|
| background: #3b82f6;
|
| }
|
|
|
| input[type="range"]:hover::-moz-range-thumb {
|
| background: #3b82f6;
|
| }
|
| `}</style>
|
| {/* Region Detection */}
|
| <button
|
| onClick={handleDetectAndShowDownload}
|
| className={`w-full flex items-center justify-between p-2 rounded hover:bg-opacity-80 ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
|
| >
|
| <span className={`text-sm ${darkMode ? 'text-gray-600' : 'text-white'}`}>
|
| Detect Regions
|
| </span>
|
| <Scan size={20} color={iconColor} />
|
| </button>
|
| </div>
|
| </div>
|
| )}
|
|
|
| {/* Main Toolbar */}
|
| <div className={`fixed top-4 left-1/2 transform -translate-x-1/2 flex items-center gap-1 ${barBgColor} rounded-lg shadow-lg p-2 z-10`}>
|
| <div className="flex items-center gap-1 pr-2 border-r border-gray-200">
|
| <button onClick={undo} className="p-2 rounded hover:bg-opacity-80" title="Undo">
|
| <Undo2 size={20} color={iconColor} />
|
| </button>
|
| <button onClick={redo} className="p-2 rounded hover:bg-opacity-80" title="Redo">
|
| <Redo2 size={20} color={iconColor} />
|
| </button>
|
| </div>
|
|
|
| <div className="flex items-center gap-1 px-2">
|
| <ColorPickerButton />
|
| <button
|
| onClick={() => setTool('selection')}
|
| className={`p-2 rounded ${tool === 'selection' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
|
| title="Selection"
|
| >
|
| <Hand size={20} color={tool === 'selection' ? '#ffffff' : iconColor} />
|
| </button>
|
| <button
|
| onClick={() => setTool('line')}
|
| className={`p-2 rounded ${tool === 'line' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
|
| title="Line"
|
| >
|
| <GripHorizontal size={20} color={tool === 'line' ? '#ffffff' : iconColor} />
|
| </button>
|
| <button
|
| onClick={() => setTool('rectangle')}
|
| className={`p-2 rounded ${tool === 'rectangle' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
|
| title="Rectangle"
|
| >
|
| <Square size={20} color={tool === 'rectangle' ? '#ffffff' : iconColor} />
|
| </button>
|
| <button
|
| onClick={() => setTool('pencil')}
|
| className={`p-2 rounded ${tool === 'pencil' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
|
| title="Pencil"
|
| >
|
| <Pencil size={20} color={tool === 'pencil' ? '#ffffff' : iconColor} />
|
| </button>
|
| <button
|
| onClick={() => setTool('eraser')}
|
| className={`p-2 rounded ${tool === 'eraser' ? 'bg-blue-500' : 'hover:bg-opacity-80'}`}
|
| title="Eraser"
|
| >
|
| <Eraser size={20} color={tool === 'eraser' ? '#ffffff' : iconColor} />
|
| </button>
|
| {(tool === 'pencil') && (
|
| <div className=' flex items-center gap-2 ml-2'>
|
| <Ruler size={16} color={iconColor} />
|
| <input
|
| type="range"
|
| min="1"
|
| max="20"
|
| value={pencilSize}
|
| onChange={(e) => setPencilSize(parseInt(e.target.value))}
|
| className="w-24 h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer"
|
| title="Pencil Size"
|
| />
|
| <span className={'text-sm ${textColor}'}>{pencilSize}px</span>
|
| </div>
|
| )}
|
| </div>
|
|
|
| <div className="flex items-center gap-1 pl-2 border-l border-gray-200">
|
| <button
|
| onClick={() => setScale(1)}
|
| className="p-2 rounded hover:bg-opacity-80"
|
| title="Reset View"
|
| >
|
| <RotateCcw size={20} color={iconColor} />
|
| </button>
|
| <button
|
| onClick={() => onZoom(-0.1)}
|
| className="p-2 rounded hover:bg-opacity-80"
|
| title="Zoom Out"
|
| >
|
| <ZoomOut size={20} color={iconColor} />
|
| </button>
|
| <span className={`text-sm min-w-[4rem] text-center ${textColor}`}>
|
| {Math.round(scale * 100)}%
|
| </span>
|
| <button
|
| onClick={() => onZoom(0.1)}
|
| className="p-2 rounded hover:bg-opacity-80"
|
| title="Zoom In"
|
| >
|
| <ZoomIn size={20} color={iconColor} />
|
| </button>
|
| </div>
|
| </div>
|
|
|
| {/* Download Button */}
|
| {showDownloadButton && (
|
| <button
|
| onClick={() => {
|
| handleDownloadRegions();
|
| setShowDownloadButton(false);
|
| }}
|
| className="fixed bottom-8 left-1/2 transform -translate-x-1/2 z-20 px-4 py-2 bg-blue-500 text-white rounded-lg shadow-lg hover:bg-blue-600 flex items-center gap-2"
|
| >
|
| <Download size={20} color="#ffffff" />
|
| Download Detected Regions
|
| </button>
|
| )}
|
|
|
|
|
| {/* Canvas Container */}
|
| <div className="absolute inset-0">
|
| <canvas
|
| ref={backgroundCanvasRef}
|
| className="absolute inset-0 z-0"
|
| />
|
| <canvas
|
| id="canvas"
|
| ref={drawingCanvasRef}
|
| onMouseDown={handleMouseDown}
|
| onMouseMove={handleMouseMove}
|
| onMouseUp={handleMouseUp}
|
| className="absolute inset-0 z-1"
|
| />
|
| </div>
|
|
|
| {/* Text Area for Writing */}
|
| {action === "writing" && (
|
| <textarea
|
| ref={textAreaRef}
|
| onBlur={handleBlur}
|
| style={{
|
| position: "fixed",
|
| top: (selectedElement.y1 - 2) * scale + panOffset.y * scale - scaleOffset.y,
|
| left: selectedElement.x1 * scale + panOffset.x * scale - scaleOffset.x,
|
| font: `${24 * scale}px sans-serif`,
|
| margin: 0,
|
| padding: 0,
|
| border: 0,
|
| outline: 0,
|
| resize: "auto",
|
| overflow: "hidden",
|
| whiteSpace: "pre",
|
| background: "transparent",
|
| color: darkMode ? 'white' : 'black',
|
| zIndex: 2,
|
| }}
|
| />
|
| )}
|
| </div>
|
| );
|
| };
|
|
|
| export default DrawingBoard; |