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