UNPKG

aura-glass

Version:

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

427 lines (424 loc) 15.5 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { AnimatePresence, motion } from 'framer-motion'; import { forwardRef, useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { useMotionPreference } from '../../hooks/useMotionPreference.js'; 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'; import { useA11yId } from '../../utils/a11y.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const GlassTessellation = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for any section titles, labels, and helper text for WCAG AA compliance tiles = [], tessellationType = "hexagonal", containerWidth = 800, containerHeight = 600, tileSize = 60, spacing = 2, animatePattern = true, morphPattern = false, morphSpeed = 2000, showGrid = false, interactive = true, onTileClick, onTileHover, glassConfig = {}, soundEnabled = true, className = "", style = {}, ...props }, ref) => { const [hoveredTile, setHoveredTile] = useState(null); const [selectedTile, setSelectedTile] = useState(null); const [morphPhase, setMorphPhase] = useState(0); const containerRef = useRef(null); const animationRef = useRef(); const { prefersReducedMotion } = useMotionPreference(); const tessellationId = useA11yId(); const { play } = useGlassSound(); // Morphing animation useEffect(() => { if (!morphPattern || prefersReducedMotion) return; const startTime = Date.now(); const animate = () => { const elapsed = Date.now() - startTime; const phase = elapsed / morphSpeed % 1; setMorphPhase(phase); animationRef.current = requestAnimationFrame(animate); }; animationRef.current = requestAnimationFrame(animate); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [morphPattern, morphSpeed, prefersReducedMotion]); // Generate tessellation pattern positions const generateTessellationPositions = useCallback(() => { const positions = new Map(); const effectiveSize = tileSize + spacing; switch (tessellationType) { case "triangular": { // Triangular tessellation const rowHeight = effectiveSize * Math.sqrt(3) / 2; let tileIndex = 0; for (let row = 0; row * rowHeight < containerHeight && tileIndex < tiles.length; row++) { const cols = Math.floor(containerWidth / effectiveSize) + row % 2; const offsetX = row % 2 ? effectiveSize / 2 : 0; for (let col = 0; col < cols && tileIndex < tiles.length; col++) { const x = col * effectiveSize + offsetX; const y = row * rowHeight; const rotation = row % 2 ? 180 : 0; positions.set(tiles[tileIndex].id, { x, y, rotation, scale: 1 }); tileIndex++; } } break; } case "square": { // Square tessellation let tileIndex = 0; const cols = Math.floor(containerWidth / effectiveSize); const rows = Math.floor(containerHeight / effectiveSize); for (let row = 0; row < rows && tileIndex < tiles.length; row++) { for (let col = 0; col < cols && tileIndex < tiles.length; col++) { const x = col * effectiveSize; const y = row * effectiveSize; positions.set(tiles[tileIndex].id, { x, y, rotation: 0, scale: 1 }); tileIndex++; } } break; } case "hexagonal": { // Hexagonal tessellation const hexWidth = effectiveSize * Math.sqrt(3); const hexHeight = effectiveSize * 2; let tileIndex = 0; for (let row = 0; row * hexHeight * 0.75 < containerHeight && tileIndex < tiles.length; row++) { const cols = Math.floor(containerWidth / hexWidth) + 1; const offsetX = row % 2 ? hexWidth / 2 : 0; for (let col = 0; col < cols && tileIndex < tiles.length; col++) { const x = col * hexWidth + offsetX; const y = row * hexHeight * 0.75; positions.set(tiles[tileIndex].id, { x, y, rotation: 0, scale: 1 }); tileIndex++; } } break; } case "rhombic": { // Rhombic tessellation const rhombWidth = effectiveSize * 1.5; const rhombHeight = effectiveSize; let tileIndex = 0; for (let row = 0; row * rhombHeight < containerHeight && tileIndex < tiles.length; row++) { const cols = Math.floor(containerWidth / rhombWidth) + 1; const offsetX = row % 2 ? rhombWidth / 2 : 0; for (let col = 0; col < cols && tileIndex < tiles.length; col++) { const x = col * rhombWidth + offsetX; const y = row * rhombHeight; const rotation = (row + col) % 2 ? 45 : -45; positions.set(tiles[tileIndex].id, { x, y, rotation, scale: 1 }); tileIndex++; } } break; } case "pentagonal": { // Pentagonal tessellation (approximation) const pentSize = effectiveSize * 0.8; let tileIndex = 0; for (let row = 0; row * pentSize < containerHeight && tileIndex < tiles.length; row++) { const cols = Math.floor(containerWidth / pentSize) + 1; const offsetX = row % 2 ? pentSize / 2 : 0; const offsetY = row % 3 ? pentSize / 3 : 0; for (let col = 0; col < cols && tileIndex < tiles.length; col++) { const x = col * pentSize + offsetX; const y = row * pentSize + offsetY; const rotation = row * col * 72 % 360; positions.set(tiles[tileIndex].id, { x, y, rotation, scale: 1 }); tileIndex++; } } break; } case "mixed": { // Mixed tessellation pattern let tileIndex = 0; const baseSize = effectiveSize * 0.7; for (let y = 0; y < containerHeight - baseSize && tileIndex < tiles.length; y += baseSize) { for (let x = 0; x < containerWidth - baseSize && tileIndex < tiles.length; x += baseSize) { const variation = Math.sin(x * 0.01 + y * 0.01 + morphPhase * Math.PI * 2); const scale = 0.8 + variation * 0.4; const rotation = variation * 60; positions.set(tiles[tileIndex].id, { x: x + variation * 20, y: y + variation * 20, rotation, scale }); tileIndex++; } } break; } } return positions; }, [tessellationType, tiles, tileSize, spacing, containerWidth, containerHeight, morphPhase]); const tilePositions = useMemo(() => generateTessellationPositions(), [generateTessellationPositions]); const handleTileClick = useCallback(tile => { setSelectedTile(tile.id); onTileClick?.(tile); if (soundEnabled) { play("click"); } setTimeout(() => setSelectedTile(null), 300); }, [onTileClick, soundEnabled, play]); const handleTileHover = useCallback(tile => { setHoveredTile(tile?.id || null); onTileHover?.(tile); if (soundEnabled && tile) { play("hover"); } }, [onTileHover, soundEnabled, play]); const renderTileShape = (tile, position) => { const isHovered = hoveredTile === tile.id; const isSelected = selectedTile === tile.id; const effectiveSize = tileSize * position.scale; const shapeProps = { className: ` transition-all duration-200 cursor-pointer ${isHovered || isSelected ? "fill-white/20 stroke-white/60" : "fill-white/10 stroke-white/30"} `, strokeWidth: 1.5 }; const pathCommands = { triangle: `M ${effectiveSize / 2} 0 L ${effectiveSize} ${effectiveSize * Math.sqrt(3) / 2} L 0 ${effectiveSize * Math.sqrt(3) / 2} Z`, square: `M 0 0 L ${effectiveSize} 0 L ${effectiveSize} ${effectiveSize} L 0 ${effectiveSize} Z`, hexagon: (() => { const points = []; for (let i = 0; i < 6; i++) { const angle = i * Math.PI / 3; const x = effectiveSize / 2 + effectiveSize / 2 * Math.cos(angle); const y = effectiveSize / 2 + effectiveSize / 2 * Math.sin(angle); points.push(`${x} ${y}`); } return `M ${points.join(" L ")} Z`; })(), rhombus: `M ${effectiveSize / 2} 0 L ${effectiveSize} ${effectiveSize / 2} L ${effectiveSize / 2} ${effectiveSize} L 0 ${effectiveSize / 2} Z`, pentagon: (() => { const points = []; for (let i = 0; i < 5; i++) { const angle = i * 2 * Math.PI / 5 - Math.PI / 2; const x = effectiveSize / 2 + effectiveSize / 2 * Math.cos(angle); const y = effectiveSize / 2 + effectiveSize / 2 * Math.sin(angle); points.push(`${x} ${y}`); } return `M ${points.join(" L ")} Z`; })(), octagon: (() => { const points = []; for (let i = 0; i < 8; i++) { const angle = i * Math.PI / 4; const x = effectiveSize / 2 + effectiveSize / 2 * Math.cos(angle); const y = effectiveSize / 2 + effectiveSize / 2 * Math.sin(angle); points.push(`${x} ${y}`); } return `M ${points.join(" L ")} Z`; })() }; return jsxs("g", { children: [jsx("path", { d: pathCommands[tile.shape] || pathCommands.hexagon, ...shapeProps }), jsx("foreignObject", { x: "0", y: "0", width: effectiveSize, height: effectiveSize, className: 'pointer-events-none', children: jsx("div", { className: 'glass-w-full glass-h-full glass-flex glass-items-center glass-justify-center glass-text-xs text-primary/90', children: tile.content }) })] }); }; const getTileVariants = () => ({ hidden: { scale: 0, opacity: 0, rotate: -180 }, visible: delay => ({ scale: 1, opacity: 1, rotate: 0, transition: { type: "spring", tension: 300, friction: 25, delay: prefersReducedMotion ? 0 : delay * 0.02 } }), hover: { scale: 1.05, transition: { type: "spring", tension: 400, friction: 20 } }, selected: { scale: 1.1, transition: { type: "spring", tension: 500, friction: 15 } } }); return jsxs(OptimizedGlassCore, { ref: ref, className: `glass-tessellation relative overflow-hidden ${className}`, style: { width: containerWidth, height: containerHeight, ...style }, glassConfig: { blur: 10, opacity: 0.95, saturation: 1.1, brightness: 1.05, ...glassConfig }, role: "application", "aria-label": `${tessellationType} tessellation pattern`, id: tessellationId, ...props, children: [jsxs("div", { ref: containerRef, className: 'absolute inset-0', children: [showGrid && jsx("div", { className: 'absolute inset-0 pointer-events-none', children: jsxs("svg", { width: containerWidth, height: containerHeight, children: [jsx("defs", { children: jsx("pattern", { id: "grid", width: "20", height: "20", patternUnits: "userSpaceOnUse", children: jsx("path", { d: "M 20 0 L 0 0 0 20", fill: "none", stroke: "white", strokeWidth: "0.5", opacity: "0.2" }) }) }), jsx("rect", { width: "100%", height: "100%", fill: "url(#grid)" })] }) }), jsx("svg", { width: containerWidth, height: containerHeight, className: 'absolute inset-0', children: jsx(AnimatePresence, { children: tiles.map((tile, index) => { const position = tilePositions.get(tile.id); if (!position) return null; const isHovered = hoveredTile === tile.id; const isSelected = selectedTile === tile.id; return jsx(motion.g, { custom: index, variants: getTileVariants(), initial: "hidden", animate: isSelected ? "selected" : isHovered ? "hover" : "visible", exit: "hidden", style: { transformOrigin: `${position.x + tileSize / 2}px ${position.y + tileSize / 2}px` }, onMouseEnter: () => interactive && handleTileHover(tile), onMouseLeave: () => interactive && handleTileHover(null), onClick: () => interactive && handleTileClick(tile), children: jsx("g", { transform: `translate(${position.x}, ${position.y}) rotate(${position.rotation + (tile.rotation || 0)})`, children: renderTileShape(tile, position) }) }, tile.id); }) }) })] }), jsxs("div", { className: 'absolute bottom-4 left-4 glass-flex glass-flex-col glass-gap-1 glass-text-xs text-primary/70', children: [jsxs("div", { className: "glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard", children: ["Pattern: ", tessellationType] }), jsxs("div", { className: "glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard", children: ["Tiles: ", tiles.length] }), jsxs("div", { className: "glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard", children: ["Size: ", tileSize, "px"] }), morphPattern && jsxs("div", { className: "glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard", children: ["Morph: ", Math.round(morphPhase * 100), "%"] })] }), jsx("div", { className: 'absolute top-4 right-4 glass-text-xs text-primary/70', children: jsxs("div", { className: "glass-surface-dark/20 glass-px-2 glass-py-1 glass-radius glass-glass-backdrop-blur-sm glass-contrast-guard", children: [tessellationType.charAt(0).toUpperCase() + tessellationType.slice(1), " ", "Tessellation"] }) })] }); }); GlassTessellation.displayName = "GlassTessellation"; export { GlassTessellation }; //# sourceMappingURL=GlassTessellation.js.map