UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

510 lines (507 loc) 19.1 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { useState, useRef, useCallback, useEffect } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; const defaultColors = ["var(--glass-white)", "var(--glass-black)", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff"]; const defaultTools = ["pen", "eraser", "rectangle", "circle", "line", "text", "select"]; const GlassWhiteboard = ({ initialData = [], collaborative = false, userId = "user", enabledTools = defaultTools, availableColors = defaultColors, width = 800, height = 600, backgroundPattern = "grid", showToolbar = true, showMinimap = false, className = "", onDrawingChange, onToolChange, onColorChange }) => { const [currentTool, setCurrentTool] = useState("pen"); const [currentColor, setCurrentColor] = useState("var(--glass-black)"); const [brushSize, setBrushSize] = useState(2); const [opacity, setOpacity] = useState(1); const [isDrawing, setIsDrawing] = useState(false); const [currentPath, setCurrentPath] = useState(null); const [currentShape, setCurrentShape] = useState(null); const [drawingData, setDrawingData] = useState(initialData); const [selectedElements, setSelectedElements] = useState(new Set()); const [isSelecting, setIsSelecting] = useState(false); const [selectionBox, setSelectionBox] = useState(null); const canvasRef = useRef(null); const overlayCanvasRef = useRef(null); const containerRef = useRef(null); // Update drawing data and notify parent const updateDrawingData = useCallback(newData => { setDrawingData(newData); onDrawingChange?.(newData); }, [onDrawingChange]); // Handle tool change const handleToolChange = tool => { setCurrentTool(tool); setSelectedElements(new Set()); setIsSelecting(false); onToolChange?.(tool); }; // Handle color change const handleColorChange = color => { setCurrentColor(color); onColorChange?.(color); }; // Get mouse/touch position relative to canvas const getCanvasPosition = useCallback(e => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; const clientY = "touches" in e ? e.touches[0].clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }, []); // Start drawing const handleMouseDown = e => { const pos = getCanvasPosition(e); setIsDrawing(true); if (currentTool === "select") { setIsSelecting(true); setSelectionBox({ x: pos.x, y: pos.y, width: 0, height: 0 }); return; } if (currentTool === "pen" || currentTool === "eraser") { const newPath = { id: `path-${Date.now()}-${Math.random()}`, tool: currentTool, points: [pos], color: currentTool === "eraser" ? "var(--glass-white)" : currentColor, width: brushSize, opacity, timestamp: Date.now() }; setCurrentPath(newPath); } else if (["rectangle", "circle", "line"].includes(currentTool)) { const newShape = { id: `shape-${Date.now()}-${Math.random()}`, type: currentTool, startX: pos.x, startY: pos.y, endX: pos.x, endY: pos.y, color: currentColor, width: brushSize, opacity, timestamp: Date.now() }; setCurrentShape(newShape); } else if (currentTool === "text") { const newShape = { id: `text-${Date.now()}-${Math.random()}`, type: "text", startX: pos.x, startY: pos.y, endX: pos.x, endY: pos.y, color: currentColor, width: brushSize, opacity, text: "", timestamp: Date.now() }; setCurrentShape(newShape); } }; // Continue drawing const handleMouseMove = useCallback(e => { if (!isDrawing) return; const pos = getCanvasPosition(e); if (currentTool === "select" && isSelecting && selectionBox) { setSelectionBox({ ...selectionBox, width: pos.x - selectionBox.x, height: pos.y - selectionBox.y }); return; } if (currentPath) { setCurrentPath({ ...currentPath, points: [...currentPath.points, pos] }); } else if (currentShape) { setCurrentShape({ ...currentShape, endX: pos.x, endY: pos.y }); } }, [isDrawing, currentTool, isSelecting, selectionBox, currentPath, currentShape, getCanvasPosition]); // Finish drawing const handleMouseUp = useCallback(() => { if (!isDrawing) return; setIsDrawing(false); if (currentTool === "select" && selectionBox && isSelecting) { // Find elements within selection box const selected = new Set(); const { x, y, width, height } = selectionBox; drawingData.forEach(item => { if ("points" in item) { // Check if path intersects with selection box const intersects = item?.points.some(point => point.x >= x && point.x <= x + width && point.y >= y && point.y <= y + height); if (intersects) selected.add(item?.id); } else { // Check if shape intersects with selection box const intersects = item?.startX >= x && item?.startX <= x + width && item?.startY >= y && item?.startY <= y + height || item?.endX >= x && item?.endX <= x + width && item?.endY >= y && item?.endY <= y + height; if (intersects) selected.add(item?.id); } }); setSelectedElements(selected); setSelectionBox(null); setIsSelecting(false); return; } if (currentPath) { updateDrawingData([...drawingData, currentPath]); setCurrentPath(null); } else if (currentShape) { updateDrawingData([...drawingData, currentShape]); setCurrentShape(null); } }, [isDrawing, currentTool, selectionBox, isSelecting, currentPath, currentShape, drawingData, updateDrawingData]); // Draw background pattern const drawBackground = useCallback(ctx => { ctx.clearRect(0, 0, width, height); switch (backgroundPattern) { case "grid": ctx.strokeStyle = "var(--glass-white)10"; ctx.lineWidth = 1; for (let x = 0; x < width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for (let y = 0; y < height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } break; case "dots": ctx.fillStyle = "var(--glass-white)20"; for (let x = 20; x < width; x += 20) { for (let y = 20; y < height; y += 20) { ctx.beginPath(); ctx.arc(x, y, 1, 0, Math.PI * 2); ctx.fill(); } } break; case "lines": ctx.strokeStyle = "var(--glass-white)10"; ctx.lineWidth = 1; for (let y = 0; y < height; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } break; } }, [width, height, backgroundPattern]); // Draw all elements const drawElements = useCallback((ctx, elements) => { elements.forEach(element => { ctx.globalAlpha = element.opacity; if ("points" in element) { // Draw path if ((element.points?.length || 0) > 1) { ctx.strokeStyle = element.color; ctx.lineWidth = element.width; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.beginPath(); ctx.moveTo(element.points[0].x, element.points[0].y); for (let i = 1; i < (element.points?.length || 0); i++) { ctx.lineTo(element.points[i].x, element.points[i].y); } ctx.stroke(); } } else { // Draw shape ctx.strokeStyle = element.color; ctx.lineWidth = element.width; ctx.fillStyle = element.color; const width = Math.abs(element.endX - element.startX); const height = Math.abs(element.endY - element.startY); const x = Math.min(element.startX, element.endX); const y = Math.min(element.startY, element.endY); switch (element.type) { case "rectangle": ctx.strokeRect(x, y, width, height); break; case "circle": const radius = Math.sqrt(width * width + height * height) / 2; const centerX = x + width / 2; const centerY = y + height / 2; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.stroke(); break; case "line": ctx.beginPath(); ctx.moveTo(element.startX, element.startY); ctx.lineTo(element.endX, element.endY); ctx.stroke(); break; case "text": if (element.text) { ctx.font = `${element.width * 10}px Arial`; ctx.fillStyle = element.color; ctx.fillText(element.text, element.startX, element.startY); } break; } } ctx.globalAlpha = 1; }); }, []); // Draw selection highlights const drawSelection = useCallback(ctx => { selectedElements.forEach(elementId => { const element = drawingData.find(item => item?.id === elementId); if (!element) return; ctx.strokeStyle = "var(--glass-white)"; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); if ("points" in element && (element.points?.length || 0) > 0) { const bounds = element.points.reduce((acc, point) => ({ minX: Math.min(acc.minX, point.x), maxX: Math.max(acc.maxX, point.x), minY: Math.min(acc.minY, point.y), maxY: Math.max(acc.maxY, point.y) }), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity }); ctx.strokeRect(bounds.minX - 10, bounds.minY - 10, bounds.maxX - bounds.minX + 20, bounds.maxY - bounds.minY + 20); } else { const bounds = { minX: Math.min(element.startX, element.endX), maxX: Math.max(element.startX, element.endX), minY: Math.min(element.startY, element.endY), maxY: Math.max(element.startY, element.endY) }; ctx.strokeRect(bounds.minX - 10, bounds.minY - 10, bounds.maxX - bounds.minX + 20, bounds.maxY - bounds.minY + 20); } ctx.setLineDash([]); }); }, [selectedElements, drawingData]); // Main render loop useEffect(() => { const canvas = canvasRef.current; const overlayCanvas = overlayCanvasRef.current; if (!canvas || !overlayCanvas) return; const ctx = canvas.getContext("2d"); const overlayCtx = overlayCanvas.getContext("2d"); if (!ctx || !overlayCtx) return; // Clear canvases ctx.clearRect(0, 0, width, height); overlayCtx.clearRect(0, 0, width, height); // Draw background drawBackground(ctx); // Draw all elements drawElements(ctx, drawingData); // Draw current path/shape being drawn if (currentPath) { drawElements(overlayCtx, [currentPath]); } if (currentShape) { drawElements(overlayCtx, [currentShape]); } // Draw selection drawSelection(ctx); // Draw selection box if (selectionBox && isSelecting) { overlayCtx.strokeStyle = "var(--glass-white)80"; overlayCtx.lineWidth = 1; overlayCtx.setLineDash([5, 5]); overlayCtx.strokeRect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height); overlayCtx.setLineDash([]); } }, [drawingData, currentPath, currentShape, selectionBox, isSelecting, drawBackground, drawElements, drawSelection, width, height]); // Handle text input const handleTextInput = useCallback(text => { if (currentShape && currentShape.type === "text") { const updatedShape = { ...currentShape, text }; setCurrentShape(updatedShape); updateDrawingData([...drawingData, updatedShape]); setCurrentShape(null); } }, [currentShape, drawingData, updateDrawingData]); // Clear canvas const clearCanvas = () => { updateDrawingData([]); setSelectedElements(new Set()); }; // Delete selected elements const deleteSelected = () => { const newData = drawingData.filter(item => !selectedElements.has(item?.id)); updateDrawingData(newData); setSelectedElements(new Set()); }; // Export canvas as image const exportAsImage = () => { const canvas = canvasRef.current; if (!canvas) return; const link = document.createElement("a"); link.download = `whiteboard-${Date.now()}.png`; link.href = canvas.toDataURL(); link.click(); }; return jsxs(OptimizedGlassCore, { "data-glass-component": true, className: cn("glass-relative", className), intent: "neutral", elevation: "level1", children: [showToolbar && jsxs("div", { className: cn("glass-flex glass-flex-wrap glass-items-center glass-gap-2 glass-p-4 glass-border-b glass-border-white-10"), children: [jsx("div", { className: cn("glass-flex glass-gap-1"), children: enabledTools.map(tool => jsx("button", { onClick: e => handleToolChange(tool), className: cn("glass-px-3 glass-py-2 glass-radius-md glass-text-sm glass-font-medium glass-transition-colors", "glass-focus glass-touch-target glass-contrast-guard", currentTool === tool ? "glass-surface-elevated glass-text-primary" : "glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle"), children: tool.charAt(0).toUpperCase() + tool.slice(1) }, tool)) }), jsx("div", { className: cn("glass-flex glass-gap-1"), children: availableColors.map((color, i) => jsx("button", { onClick: e => handleColorChange(color), className: cn("glass-w-8 glass-h-8 glass-radius-md glass-border-2 glass-transition-all", "glass-focus glass-touch-target glass-contrast-guard", currentColor === color ? "glass-border-white glass-scale-110" : "glass-border-white-30 glass-hover-border-white-60"), style: { backgroundColor: color } }, `${color}-${i}`)) }), jsxs("div", { className: cn("glass-flex glass-items-center glass-gap-2"), children: [jsx("span", { className: cn("glass-text-sm glass-text-primary-70"), children: "Size:" }), jsx("input", { type: "range", min: "1", max: "20", value: brushSize, onChange: e => setBrushSize(Number(e.target.value)), className: cn("glass-w-16 glass-focus glass-touch-target glass-contrast-guard") }), jsx("span", { className: cn("glass-text-sm glass-text-primary-70 glass-w-6"), children: brushSize })] }), jsxs("div", { className: cn("glass-flex glass-items-center glass-gap-2"), children: [jsx("span", { className: cn("glass-text-sm glass-text-primary-70"), children: "Opacity:" }), jsx("input", { type: "range", min: "0.1", max: "1", step: "0.1", value: opacity, onChange: e => setOpacity(Number(e.target.value)), className: cn("glass-w-16 glass-focus glass-touch-target glass-contrast-guard") }), jsxs("span", { className: cn("glass-text-sm glass-text-primary-70 glass-w-8"), children: [(opacity * 100).toFixed(0), "%"] })] }), jsxs("div", { className: cn("glass-flex glass-gap-1 glass-ml-4"), children: [jsx("button", { onClick: clearCanvas, className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"), children: "Clear" }), selectedElements.size > 0 && jsx("button", { onClick: deleteSelected, className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-red-400 glass-hover-text-red-300 glass-hover-surface-red-10 glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"), children: "Delete Selected" }), jsx("button", { onClick: exportAsImage, className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"), children: "Export" })] })] }), currentShape && currentShape.type === "text" && jsx("div", { className: cn("glass-absolute glass-inset-0 glass-surface-black-50 glass-flex glass-items-center glass-justify-center glass-z-20"), children: jsx(OptimizedGlassCore, { className: cn("glass-p-6 glass-max-w-sm glass-w-full glass-mx-4"), blur: "medium", elevation: "level2", children: jsx("input", { autoFocus: true, placeholder: "Enter text...", className: cn("glass-w-full glass-px-3 glass-py-2 glass-surface-white-10 glass-border glass-border-white-20 glass-radius-md glass-text-primary glass-placeholder-white-50 glass-focus-outline-none glass-focus-border-white-40 glass-focus glass-touch-target glass-contrast-guard"), onKeyDown: e => { if (e.key === "Enter") { handleTextInput(e.target.value); } else if (e.key === "Escape") { setCurrentShape(null); } } }) }) }), jsxs("div", { ref: containerRef, className: cn("glass-relative glass-overflow-hidden glass-surface-black-20"), style: { width, height }, children: [jsx("canvas", { ref: canvasRef, width: width, height: height, className: cn("glass-absolute glass-inset-0 glass-cursor-crosshair"), onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp }), jsx("canvas", { ref: overlayCanvasRef, width: width, height: height, className: cn("glass-absolute glass-inset-0 glass-pointer-events-none") })] })] }); }; export { GlassWhiteboard }; //# sourceMappingURL=GlassWhiteboard.js.map