UNPKG

aura-glass

Version:

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

409 lines (406 loc) 14.9 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useRef, useState, useEffect, useCallback } 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 { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const GlassDrawingCanvas = /*#__PURE__*/forwardRef(({ width = 800, height = 600, tool = { type: "pen", size: 2, color: "var(--glass-black)", opacity: 1 }, readOnly = false, backgroundColor = "transparent", backgroundImage, showGrid = false, gridSize = 20, pressureSensitive = true, smoothStrokes = true, maxHistory = 50, data = [], onChange, onStrokeComplete, onExport, availableTools = ["pen", "brush", "eraser", "line", "rectangle", "circle"], toolPanelPosition = "top", showToolPanel = true, respectMotionPreference = true, className, ...props }, ref) => { const { prefersReducedMotion, isMotionSafe } = useMotionPreferenceContext(); const { play } = useGlassSound(); const canvasRef = useRef(null); const contextRef = useRef(null); const drawingCanvasId = useA11yId("glass-drawing-canvas"); const [strokes, setStrokes] = useState(data); const [currentStroke, setCurrentStroke] = useState(null); const [isDrawing, setIsDrawing] = useState(false); const [history, setHistory] = useState([data]); const [historyIndex, setHistoryIndex] = useState(0); const [currentTool, setCurrentTool] = useState(tool); // Initialize canvas useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const context = canvas.getContext("2d"); if (!context) return; contextRef.current = context; // Set canvas size canvas.width = width; canvas.height = height; // Configure context context.lineCap = "round"; context.lineJoin = "round"; context.imageSmoothingEnabled = true; }, [width, height]); // Draw grid const drawGrid = useCallback(context => { if (!showGrid) return; context.save(); context.strokeStyle = "rgba(200, 200, 200, 0.3)"; context.lineWidth = 0.5; for (let x = 0; x <= width; x += gridSize) { context.beginPath(); context.moveTo(x, 0); context.lineTo(x, height); context.stroke(); } for (let y = 0; y <= height; y += gridSize) { context.beginPath(); context.moveTo(0, y); context.lineTo(width, y); context.stroke(); } context.restore(); }, [showGrid, width, height, gridSize]); // Draw background const drawBackground = useCallback(context => { context.save(); if (backgroundColor && backgroundColor !== "transparent") { context.fillStyle = backgroundColor; context.fillRect(0, 0, width, height); } else { context.clearRect(0, 0, width, height); } context.restore(); }, [backgroundColor, backgroundImage, width, height]); // Draw stroke const drawStroke = useCallback((context, stroke) => { if (stroke.points.length < 2) return; context.save(); context.globalAlpha = stroke.tool.opacity; context.strokeStyle = stroke.tool.color; context.lineWidth = stroke.tool.size; if (stroke.tool.type === "eraser") { context.globalCompositeOperation = "destination-out"; } context.beginPath(); if (smoothStrokes && stroke.points.length > 2) { // Smooth the stroke using quadratic curves context.moveTo(stroke.points[0].x, stroke.points[0].y); for (let i = 1; i < stroke.points.length - 1; i++) { const current = stroke.points[i]; const next = stroke.points[i + 1]; const controlX = current.x; const controlY = current.y; const endX = (current.x + next.x) / 2; const endY = (current.y + next.y) / 2; context.quadraticCurveTo(controlX, controlY, endX, endY); } // Draw the last segment const lastPoint = stroke.points[stroke.points.length - 1]; const secondLastPoint = stroke.points[stroke.points.length - 2]; context.quadraticCurveTo(secondLastPoint.x, secondLastPoint.y, lastPoint.x, lastPoint.y); } else { // Draw straight lines context.moveTo(stroke.points[0].x, stroke.points[0].y); for (let i = 1; i < stroke.points.length; i++) { context.lineTo(stroke.points[i].x, stroke.points[i].y); } } context.stroke(); context.restore(); }, [smoothStrokes]); // Redraw canvas const redrawCanvas = useCallback(() => { const context = contextRef.current; if (!context) return; drawBackground(context); drawGrid(context); // Draw all strokes strokes.forEach(stroke => drawStroke(context, stroke)); // Draw current stroke if (currentStroke) { drawStroke(context, currentStroke); } }, [strokes, currentStroke, drawBackground, drawGrid, drawStroke]); // Update canvas when strokes change useEffect(() => { redrawCanvas(); }, [redrawCanvas]); // Get pointer position const getPointerPos = useCallback(event => { const canvas = canvasRef.current; if (!canvas) return { x: 0, y: 0 }; const rect = canvas.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top, pressure: event.pressure || 1 }; }, []); // Start drawing const handlePointerDown = useCallback(event => { if (readOnly) return; event.preventDefault(); setIsDrawing(true); const pos = getPointerPos(event); const newStroke = { id: `stroke-${Date.now()}-${Math.random()}`, tool: currentTool, points: [pos], timestamp: Date.now() }; setCurrentStroke(newStroke); play("tap"); }, [readOnly, getPointerPos, currentTool, play]); // Continue drawing const handlePointerMove = useCallback(event => { if (!isDrawing || !currentStroke || readOnly) return; const pos = getPointerPos(event); const updatedStroke = { ...currentStroke, points: [...currentStroke.points, pos] }; setCurrentStroke(updatedStroke); }, [isDrawing, currentStroke, readOnly, getPointerPos]); // End drawing const handlePointerUp = useCallback(() => { if (!isDrawing || !currentStroke) return; setIsDrawing(false); // Add completed stroke to strokes const newStrokes = [...strokes, currentStroke]; setStrokes(newStrokes); // Update history const newHistory = history.slice(0, historyIndex + 1); newHistory.push(newStrokes); if (newHistory.length > maxHistory) { newHistory.shift(); } setHistory(newHistory); setHistoryIndex(newHistory.length - 1); // Callbacks onChange?.(newStrokes); onStrokeComplete?.(currentStroke); setCurrentStroke(null); play("success"); }, [isDrawing, currentStroke, strokes, history, historyIndex, maxHistory, onChange, onStrokeComplete, play]); // Undo const undo = useCallback(() => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); setStrokes(history[newIndex]); onChange?.(history[newIndex]); play("tap"); } }, [historyIndex, history, onChange, play]); // Redo const redo = useCallback(() => { if (historyIndex < history.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); setStrokes(history[newIndex]); onChange?.(history[newIndex]); play("tap"); } }, [historyIndex, history, onChange, play]); // Clear canvas const clear = useCallback(() => { const newStrokes = []; setStrokes(newStrokes); const newHistory = [...history, newStrokes]; setHistory(newHistory); setHistoryIndex(newHistory.length - 1); onChange?.(newStrokes); play("error"); }, [history, onChange, play]); // Export canvas const exportCanvas = useCallback((format = "png") => { const canvas = canvasRef.current; if (!canvas) return; const dataUrl = canvas.toDataURL(`image/${format}`); onExport?.(dataUrl, format); play("success"); }, [onExport, play]); // Tool panel const renderToolPanel = () => { if (!showToolPanel) return null; return jsxs(OptimizedGlassCore, { elevation: "level2", intensity: "medium", depth: 1, tint: "neutral", border: "subtle", className: "glass-tool-panel glass-flex glass-items-center glass-gap-2 glass-p-2 glass-radius-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard", children: [jsx("div", { className: "glass-flex glass-gap-1", children: availableTools.map(toolType => jsx("button", { onClick: () => setCurrentTool({ ...currentTool, type: toolType }), className: cn("glass-p-2 glass-radius-md transition-all duration-200", "hover:bg-background/20 focus:outline-none focus:ring-2 focus:ring-primary/50", "glass-focus glass-touch-target glass-contrast-guard", currentTool.type === toolType && "bg-primary/20 text-primary"), title: toolType.charAt(0).toUpperCase() + toolType.slice(1), children: jsx("span", { className: 'w-4 h-4 block', children: toolType[0].toUpperCase() }) }, toolType)) }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: "glass-text-sm glass-text-secondary", children: "Size:" }), jsx("input", { type: "range", min: "1", max: "50", value: currentTool.size, onChange: e => setCurrentTool({ ...currentTool, size: parseInt(e.target.value) }), className: 'w-20 glass-focus glass-touch-target glass-contrast-guard' }), jsx("span", { className: 'glass-text-sm min-w-[2ch]', children: currentTool.size })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: "glass-text-sm glass-text-secondary", children: "Color:" }), jsx("input", { type: "color", value: currentTool.color, onChange: e => setCurrentTool({ ...currentTool, color: e.target.value }), className: 'w-8 h-8 glass-radius-md glass-border glass-border-glass-border/20 glass-focus glass-touch-target glass-contrast-guard' })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: "glass-text-sm glass-text-secondary", children: "Opacity:" }), jsx("input", { type: "range", min: "0.1", max: "1", step: "0.1", value: currentTool.opacity, onChange: e => setCurrentTool({ ...currentTool, opacity: parseFloat(e.target.value) }), className: 'w-20 glass-focus glass-touch-target glass-contrast-guard' })] }), jsxs("div", { className: 'glass-flex glass-gap-1 ml-auto', children: [jsx("button", { onClick: undo, disabled: historyIndex <= 0, className: 'glass-p-2 glass-radius-md hover:glass-surface-overlay disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', title: "Undo", children: "\u21B6" }), jsx("button", { onClick: redo, disabled: historyIndex >= history.length - 1, className: 'glass-p-2 glass-radius-md hover:glass-surface-overlay disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-contrast-guard', title: "Redo", children: "\u21B7" }), jsx("button", { onClick: clear, className: 'glass-p-2 glass-radius-md hover:glass-surface-overlay text-primary glass-focus glass-touch-target glass-contrast-guard', title: "Clear", children: "\uD83D\uDDD1" }), jsx("button", { onClick: () => exportCanvas("png"), className: 'glass-p-2 glass-radius-md hover:glass-surface-overlay glass-focus glass-touch-target glass-contrast-guard', title: "Export", children: "\uD83D\uDCBE" })] })] }); }; return jsx(OptimizedGlassCore, { ref: ref, id: drawingCanvasId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-drawing-canvas relative glass-radius-lg glass-backdrop-blur-md border border-border/20", className), ...props, children: jsxs(MotionFramer, { preset: isMotionSafe && respectMotionPreference ? "fadeIn" : "none", className: "glass-flex glass-flex-col glass-gap-4 glass-p-4", children: [(toolPanelPosition === "top" || toolPanelPosition === "bottom") && renderToolPanel(), jsxs("div", { className: "glass-flex glass-gap-4", children: [toolPanelPosition === "left" && jsx("div", { className: "glass-flex glass-flex-col", children: renderToolPanel() }), jsxs("div", { className: 'relative', children: [jsx("canvas", { ref: canvasRef, width: width, height: height, className: cn("border border-border/20 glass-radius-md bg-white", !readOnly && "cursor-crosshair", "touch-none" // Prevent touch scrolling ), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: handlePointerUp, style: { width, height } }), toolPanelPosition === "floating" && jsx("div", { className: 'absolute top-4 right-4', children: renderToolPanel() })] }), toolPanelPosition === "right" && jsx("div", { className: "glass-flex glass-flex-col", children: renderToolPanel() })] }), toolPanelPosition === "bottom" && renderToolPanel()] }) }); }); GlassDrawingCanvas.displayName = "GlassDrawingCanvas"; export { GlassDrawingCanvas, GlassDrawingCanvas as default }; //# sourceMappingURL=GlassDrawingCanvas.js.map