'use client'; import { useEffect, useRef, useState } from 'react'; import { useCanvasStore, useLayerStore, useToolStore } from '@/store'; import { useHistoryStore } from '@/store/history-store'; import { useSelectionStore } from '@/store/selection-store'; import { drawMarchingAnts } from '@/lib/selection-utils'; import { getContext, drawGrid, drawCheckerboard } from '@/lib/canvas-utils'; import { DrawCommand } from '@/core/commands'; import { PencilTool, BrushTool, EraserTool, FillTool, EyedropperTool, RectangularSelectionTool, EllipticalSelectionTool, LassoSelectionTool, MagicWandTool, MoveTool, FreeTransformTool, type BaseTool, } from '@/tools'; import type { PointerState } from '@/types'; import { cn } from '@/lib/utils'; // Tool instances const tools: Record = { pencil: new PencilTool(), brush: new BrushTool(), eraser: new EraserTool(), fill: new FillTool(), eyedropper: new EyedropperTool(), select: new RectangularSelectionTool(), 'rectangular-select': new RectangularSelectionTool(), 'elliptical-select': new EllipticalSelectionTool(), 'lasso-select': new LassoSelectionTool(), 'magic-wand': new MagicWandTool(), move: new MoveTool(), transform: new FreeTransformTool(), }; export function CanvasWithTools() { const canvasRef = useRef(null); const containerRef = useRef(null); const drawCommandRef = useRef(null); const { width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, screenToCanvas, } = useCanvasStore(); const { layers, getActiveLayer } = useLayerStore(); const { activeTool, settings } = useToolStore(); const { executeCommand } = useHistoryStore(); const { activeSelection, selectionType, isMarching } = useSelectionStore(); const [marchingOffset, setMarchingOffset] = useState(0); const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState({ x: 0, y: 0 }); const [pointer, setPointer] = useState({ isDown: false, x: 0, y: 0, prevX: 0, prevY: 0, pressure: 1, }); // Render canvas useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = getContext(canvas); const container = containerRef.current; if (!container) return; // Set canvas size to match container const rect = container.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Save context state ctx.save(); // Apply transformations ctx.translate(offsetX + canvas.width / 2, offsetY + canvas.height / 2); ctx.scale(zoom, zoom); ctx.translate(-width / 2, -height / 2); // Draw checkerboard background drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0'); // Draw background color if not transparent if (backgroundColor && backgroundColor !== 'transparent') { ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, width, height); } // Draw all visible layers layers .filter((layer) => layer.visible && layer.canvas) .sort((a, b) => a.order - b.order) .forEach((layer) => { if (!layer.canvas) return; ctx.globalAlpha = layer.opacity; ctx.globalCompositeOperation = layer.blendMode as GlobalCompositeOperation; ctx.drawImage(layer.canvas, layer.x, layer.y); }); // Reset composite operation ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; // Draw grid if enabled if (showGrid) { drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)'); } // Draw selection if active (marching ants) if (activeSelection && isMarching) { drawMarchingAnts(ctx, activeSelection.mask, marchingOffset); } // Restore context state ctx.restore(); }, [layers, width, height, zoom, offsetX, offsetY, showGrid, gridSize, backgroundColor, selection, pointer, activeSelection, isMarching, marchingOffset]); // Marching ants animation useEffect(() => { if (!activeSelection || !isMarching) return; const interval = setInterval(() => { setMarchingOffset((prev) => (prev + 1) % 8); }, 50); return () => clearInterval(interval); }, [activeSelection, isMarching]); // Handle mouse wheel for zooming const handleWheel = (e: React.WheelEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const { zoomIn, zoomOut } = useCanvasStore.getState(); if (e.deltaY < 0) { zoomIn(); } else { zoomOut(); } } }; // Handle pointer down const handlePointerDown = (e: React.PointerEvent) => { const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; const screenX = e.clientX - rect.left; const screenY = e.clientY - rect.top; const canvasPos = screenToCanvas(screenX, screenY); // Check for panning if (e.button === 1 || (e.button === 0 && e.shiftKey)) { setIsPanning(true); setPanStart({ x: e.clientX - offsetX, y: e.clientY - offsetY }); e.preventDefault(); return; } // Transform tools const transformTools = ['move', 'transform']; if (e.button === 0 && !e.shiftKey && transformTools.includes(activeTool)) { const tool = tools[activeTool]; const newPointer: PointerState = { isDown: true, x: canvasPos.x, y: canvasPos.y, prevX: canvasPos.x, prevY: canvasPos.y, pressure: e.pressure || 1, }; setPointer(newPointer); tool.onPointerDown(newPointer, {} as any, settings); return; } // Selection tools const selectionTools = ['select', 'rectangular-select', 'elliptical-select', 'lasso-select', 'magic-wand']; if (e.button === 0 && !e.shiftKey && selectionTools.includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas) return; const tool = tools[`${selectionType}-select`] || tools['select']; const newPointer: PointerState = { isDown: true, x: canvasPos.x, y: canvasPos.y, prevX: canvasPos.x, prevY: canvasPos.y, pressure: e.pressure || 1, }; setPointer(newPointer); const ctx = activeLayer.canvas.getContext('2d'); if (ctx) { tool.onPointerDown(newPointer, ctx, settings); } return; } // Drawing tools if (e.button === 0 && !e.shiftKey && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas || activeLayer.locked) return; const newPointer: PointerState = { isDown: true, x: canvasPos.x, y: canvasPos.y, prevX: canvasPos.x, prevY: canvasPos.y, pressure: e.pressure || 1, }; setPointer(newPointer); // Create draw command for history drawCommandRef.current = new DrawCommand(activeLayer.id, tools[activeTool].name); // Call tool's onPointerDown const ctx = activeLayer.canvas.getContext('2d'); if (ctx) { tools[activeTool].onPointerDown(newPointer, ctx, settings); } } }; // Handle pointer move const handlePointerMove = (e: React.PointerEvent) => { const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; const screenX = e.clientX - rect.left; const screenY = e.clientY - rect.top; const canvasPos = screenToCanvas(screenX, screenY); // Panning if (isPanning) { const { setPanOffset } = useCanvasStore.getState(); setPanOffset(e.clientX - panStart.x, e.clientY - panStart.y); return; } // Drawing if (pointer.isDown && ['pencil', 'brush', 'eraser', 'eyedropper'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas) return; const newPointer: PointerState = { ...pointer, x: canvasPos.x, y: canvasPos.y, pressure: e.pressure || 1, }; setPointer(newPointer); const ctx = activeLayer.canvas.getContext('2d'); if (ctx) { tools[activeTool].onPointerMove(newPointer, ctx, settings); } } }; // Handle pointer up const handlePointerUp = (e: React.PointerEvent) => { if (isPanning) { setIsPanning(false); return; } if (pointer.isDown && ['pencil', 'brush', 'eraser', 'fill', 'eyedropper'].includes(activeTool)) { const activeLayer = getActiveLayer(); if (!activeLayer || !activeLayer.canvas) return; const ctx = activeLayer.canvas.getContext('2d'); if (ctx) { tools[activeTool].onPointerUp(pointer, ctx, settings); } // Capture after state and add to history if (drawCommandRef.current) { drawCommandRef.current.captureAfterState(); executeCommand(drawCommandRef.current); drawCommandRef.current = null; } setPointer({ ...pointer, isDown: false }); } }; return (
); }