aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
409 lines (406 loc) • 14.9 kB
JavaScript
'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