FlowWeb / src /features /canvas /components /nodes /DrawNode.jsx
danylokhodus's picture
Refactor canvas tools: Fixed drawing logic, improved text input, and added local image drag-and-drop support
afbb4df
Raw
History Blame Contribute Delete
5.59 kB
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { NodeResizer, Handle, Position } from '@xyflow/react';
import { Pencil, Eraser, Trash2 } from 'lucide-react';
import { useTheme } from '../../../../context/ThemeContext';
const DrawNode = ({ id, data, selected }) => {
const { isDarkMode } = useTheme();
const canvasRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const [tool, setTool] = useState('pencil');
// Use a ref for drawing properties to avoid re-renders during active draw
const drawState = useRef({
ctx: null,
lastX: 0,
lastY: 0
});
const initCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
drawState.current.ctx = ctx;
// Use absolute size from data or measure parent
const rect = canvas.getBoundingClientRect();
if (canvas.width !== rect.width || canvas.height !== rect.height) {
// Save current content before resizing
const tempImage = canvas.toDataURL();
canvas.width = rect.width;
canvas.height = rect.height;
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.src = tempImage || data.image;
}
if (data.image && !isDrawing) {
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.src = data.image;
}
}, [data.image, isDrawing]);
useEffect(() => {
initCanvas();
}, [initCanvas, data.width, data.height]);
const getCoordinates = (e) => {
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
// Support both mouse and touch
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
return {
x: clientX - rect.left,
y: clientY - rect.top
};
};
const startDrawing = (e) => {
if (!selected) return;
setIsDrawing(true);
const { x, y } = getCoordinates(e);
drawState.current.lastX = x;
drawState.current.lastY = y;
};
const draw = (e) => {
if (!isDrawing || !selected) return;
// Prevent canvas movement while drawing
e.preventDefault();
e.stopPropagation();
const { x, y } = getCoordinates(e);
const ctx = drawState.current.ctx;
ctx.beginPath();
ctx.moveTo(drawState.current.lastX, drawState.current.lastY);
ctx.lineTo(x, y);
ctx.strokeStyle = tool === 'eraser' ? (isDarkMode ? '#1e1e1e' : '#ffffff') : (data.strokeColor || (isDarkMode ? '#ffffff' : '#000000'));
ctx.lineWidth = tool === 'eraser' ? 20 : (data.lineWidth || 3);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = tool === 'eraser' ? 'destination-out' : 'source-over';
ctx.stroke();
drawState.current.lastX = x;
drawState.current.lastY = y;
};
const stopDrawing = () => {
if (!isDrawing) return;
setIsDrawing(false);
const canvas = canvasRef.current;
if (data.updateNodeData) {
data.updateNodeData(id, { image: canvas.toDataURL() });
}
};
const clearCanvas = (e) => {
e.stopPropagation();
const ctx = drawState.current.ctx;
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
if (data.updateNodeData) {
data.updateNodeData(id, { image: null });
}
};
return (
<div
className={`relative group transition-all rounded-xl ${
selected ? 'ring-2 ring-primary shadow-2xl bg-surface-container-low/20' : 'hover:bg-primary/5'
}`}
style={{
width: data.width || 400,
height: data.height || 300,
}}
>
<NodeResizer
isVisible={selected}
minWidth={100}
minHeight={100}
onResizeEnd={() => initCanvas()}
/>
{selected && (
<div
className="absolute -top-12 left-1/2 -translate-x-1/2 flex items-center gap-1 bg-surface-container-lowest border-2 border-primary rounded-xl px-2 py-1 shadow-lg z-[100]"
onMouseDown={e => e.stopPropagation()} // Prevent node drag when clicking toolbar
>
<button
onClick={() => setTool('pencil')}
className={`p-1.5 rounded-lg ${tool === 'pencil' ? 'bg-primary text-white' : 'text-primary hover:bg-surface-variant'}`}
>
<Pencil size={14} />
</button>
<button
onClick={() => setTool('eraser')}
className={`p-1.5 rounded-lg ${tool === 'eraser' ? 'bg-primary text-white' : 'text-primary hover:bg-surface-variant'}`}
>
<Eraser size={14} />
</button>
<div className="w-[1px] h-4 bg-primary/20 mx-1"></div>
<button
onClick={clearCanvas}
className="p-1.5 rounded-lg text-red-500 hover:bg-red-500/10"
>
<Trash2 size={14} />
</button>
</div>
)}
<canvas
ref={canvasRef}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
className={`block w-full h-full touch-none ${selected ? 'cursor-crosshair' : 'cursor-default pointer-events-none'}`}
/>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<Handle type="source" position={Position.Bottom} className="!opacity-0" />
</div>
);
};
export default DrawNode;