aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
472 lines (469 loc) • 15.7 kB
JavaScript
'use client';
import { useRef, useState, useEffect, useCallback } from 'react';
import { ZSpaceManager, zSpaceLayer } from '../../core/mixins/zSpaceLayer.js';
import { useMultiSpring } from '../../animations/hooks/useMultiSpringBasic.js';
const DEFAULT_CONFIG = {
enabled: true,
springConfig: {
stiffness: 200,
damping: 25,
mass: 1
},
duration: 300,
layered: true,
staggerDelay: 50,
maxConcurrent: 5,
depthScaling: true,
depthScaleFactor: 0.02,
depthBlur: true,
maxBlur: 5
};
function useZSpaceAnimation(initialLayers = [], config = {}) {
const finalConfig = {
...DEFAULT_CONFIG,
...config
};
const zSpaceManagerRef = useRef(ZSpaceManager.getInstance());
const [layers, setLayers] = useState(initialLayers);
const [animatingLayers, setAnimatingLayers] = useState(new Set());
const springSystem = useMultiSpring({}, {
config: finalConfig.springConfig
});
// Initialize layers with z-space management
useEffect(() => {
initialLayers.forEach(layer => {
zSpaceManagerRef.current.registerLayer(layer.id, layer.zIndex);
});
setLayers(initialLayers);
return () => {
initialLayers.forEach(layer => {
zSpaceManagerRef.current.unregisterLayer(layer.id);
});
};
}, [initialLayers]);
// Animate layer to new z-space position
const animateToZSpace = useCallback((layerId, targetElevation, options = {}) => {
if (!finalConfig.enabled) return;
const layer = layers.find(l => l.id === layerId);
if (!layer) return;
const {
duration = finalConfig.duration,
onComplete
} = options;
// Mark as animating
setAnimatingLayers(prev => new Set(prev).add(layerId));
// Create spring animation targets
const springTargets = {
[`${layerId}-elevation`]: targetElevation,
[`${layerId}-opacity`]: targetElevation > layer.elevation ? 1 : 0.8,
[`${layerId}-scale`]: finalConfig.depthScaling ? 1 + targetElevation * finalConfig.depthScaleFactor : 1,
[`${layerId}-blur`]: finalConfig.depthBlur ? Math.min(finalConfig.maxBlur, targetElevation * 0.1) : 0
};
springSystem.start(springTargets);
// Update layer after animation
setTimeout(() => {
setLayers(prev => prev.map(l => l.id === layerId ? {
...l,
elevation: targetElevation,
zIndex: zSpaceManagerRef.current.getLayerZIndex(layerId),
opacity: springTargets?.[`${layerId}-opacity`],
scale: springTargets?.[`${layerId}-scale`],
blur: springTargets?.[`${layerId}-blur`],
transform: `scale(${springTargets?.[`${layerId}-scale`]}) translateZ(${targetElevation}px)`
} : l));
setAnimatingLayers(prev => {
const next = new Set(prev);
next.delete(layerId);
return next;
});
onComplete?.();
}, duration);
}, [layers, finalConfig, springSystem]);
// Animate layered transition
const animateLayeredTransition = useCallback((layerUpdates, globalOptions = {}) => {
if (!finalConfig.enabled || !finalConfig.layered) return;
const {
staggerDelay = finalConfig.staggerDelay,
duration
} = globalOptions;
layerUpdates.forEach((update, index) => {
const delay = update.delay ?? index * staggerDelay;
setTimeout(() => {
animateToZSpace(update.id, update.elevation, {
duration
});
}, delay);
});
}, [finalConfig, animateToZSpace]);
// Bring layer to front
const bringToFront = useCallback((layerId, options) => {
const maxElevation = Math.max(...layers.map(l => l.elevation)) + 10;
animateToZSpace(layerId, maxElevation, options);
}, [layers, animateToZSpace]);
// Send layer to back
const sendToBack = useCallback((layerId, options) => {
const minElevation = Math.min(...layers.map(l => l.elevation)) - 10;
animateToZSpace(layerId, minElevation, options);
}, [layers, animateToZSpace]);
// Create depth-based stacking
const createDepthStack = useCallback((layerIds, baseElevation = 0, elevationStep = 5) => {
const updates = layerIds.map((id, index) => ({
id,
elevation: baseElevation + index * elevationStep
}));
animateLayeredTransition(updates);
}, [animateLayeredTransition]);
// Animate focus transition
const animateFocusTransition = useCallback((focusedLayerId, backgroundLayers, options = {}) => {
const {
focusElevation = 20,
backgroundElevation = -5,
duration = finalConfig.duration
} = options;
// Bring focused layer to front
animateToZSpace(focusedLayerId, focusElevation, {
duration
});
// Push background layers back
setTimeout(() => {
backgroundLayers.forEach(layerId => {
animateToZSpace(layerId, backgroundElevation, {
duration: duration * 0.8
});
});
}, duration * 0.2);
}, [animateToZSpace, finalConfig.duration]);
// Get layer styles for rendering
const getLayerStyles = useCallback(layerId => {
const layer = layers.find(l => l.id === layerId);
if (!layer) return {};
const isAnimating = animatingLayers.has(layerId);
const springValues = springSystem.values;
// Use spring values if animating, otherwise use layer values
const elevation = isAnimating ? springValues[`${layerId}-elevation`] ?? layer.elevation : layer.elevation;
const opacity = isAnimating ? springValues[`${layerId}-opacity`] ?? layer.opacity : layer.opacity;
const scale = isAnimating ? springValues[`${layerId}-scale`] ?? layer.scale : layer.scale;
const blur = isAnimating ? springValues[`${layerId}-blur`] ?? layer.blur : layer.blur;
return {
...zSpaceLayer({
zIndex: zSpaceManagerRef.current.getLayerZIndex(layerId),
elevation,
opacity
}),
transform: `scale(${scale}) translateZ(${elevation}px)`,
filter: blur > 0 ? `blur(${blur}px)` : 'none',
transition: finalConfig.enabled ? `all ${finalConfig.duration}ms ease` : 'none'
};
}, [layers, animatingLayers, springSystem.values, finalConfig]);
// Add new layer
const addLayer = useCallback((layer, options = {}) => {
const {
animateIn = true,
initialElevation = 0,
duration
} = options;
const newLayer = {
...layer,
zIndex: zSpaceManagerRef.current.registerLayer(layer.id),
elevation: initialElevation,
opacity: 0,
scale: 0.8,
blur: 0,
transform: `scale(0.8) translateZ(${initialElevation}px)`
};
setLayers(prev => [...prev, newLayer]);
if (animateIn) {
setTimeout(() => {
animateToZSpace(layer.id, layer.elevation, {
duration
});
}, 16); // Next frame
}
}, [animateToZSpace]);
// Remove layer
const removeLayer = useCallback((layerId, options = {}) => {
const {
animateOut = true,
duration,
onComplete
} = options;
if (animateOut) {
animateToZSpace(layerId, -20, {
duration,
onComplete: () => {
setLayers(prev => prev.filter(l => l.id !== layerId));
zSpaceManagerRef.current.unregisterLayer(layerId);
onComplete?.();
}
});
} else {
setLayers(prev => prev.filter(l => l.id !== layerId));
zSpaceManagerRef.current.unregisterLayer(layerId);
onComplete?.();
}
}, [animateToZSpace]);
// Update layer properties
const updateLayer = useCallback((layerId, updates, options = {}) => {
const {
animate = true,
duration
} = options;
if (animate && updates.elevation !== undefined) {
animateToZSpace(layerId, updates.elevation, {
duration
});
} else {
setLayers(prev => prev.map(layer => layer.id === layerId ? {
...layer,
...updates
} : layer));
}
}, [animateToZSpace]);
// Get layer information
const getLayer = useCallback(layerId => {
return layers.find(l => l.id === layerId);
}, [layers]);
// Get all layer information
const getAllLayers = useCallback(() => {
return [...layers];
}, [layers]);
// Check if layer is animating
const isLayerAnimating = useCallback(layerId => {
return animatingLayers.has(layerId);
}, [animatingLayers]);
// Get animation progress
const getAnimationProgress = useCallback(layerId => {
if (!isLayerAnimating(layerId)) return 1;
const springValues = springSystem.values;
const targetElevation = layers.find(l => l.id === layerId)?.elevation ?? 0;
const currentElevation = springValues[`${layerId}-elevation`] ?? targetElevation;
// Simple progress calculation
return Math.abs(currentElevation / targetElevation) || 0;
}, [isLayerAnimating, springSystem.values, layers]);
return {
layers,
animatingLayers,
// Layer management
addLayer,
removeLayer,
updateLayer,
getLayer,
getAllLayers,
// Animation methods
animateToZSpace,
animateLayeredTransition,
bringToFront,
sendToBack,
createDepthStack,
animateFocusTransition,
// Style methods
getLayerStyles,
// State methods
isLayerAnimating,
getAnimationProgress,
// Configuration
config: finalConfig,
zSpaceManager: zSpaceManagerRef.current
};
}
// Hook for parallax z-space effects
function useParallaxZSpace(layers, config = {}) {
const {
mouseInfluence = 0.1,
scrollInfluence = 0.5,
...zSpaceConfig
} = config;
const [mousePosition, setMousePosition] = useState({
x: 0,
y: 0
});
const [scrollPosition, setScrollPosition] = useState(0);
const zSpaceAnimation = useZSpaceAnimation(layers.map(layer => ({
id: layer.id,
zIndex: 1,
elevation: 0,
opacity: 1,
scale: 1,
blur: 0,
transform: ''
})), zSpaceConfig);
// Mouse parallax effect
useEffect(() => {
const handleMouseMove = event => {
const x = (event.clientX / window.innerWidth - 0.5) * 2;
const y = (event.clientY / window.innerHeight - 0.5) * 2;
setMousePosition({
x,
y
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Scroll parallax effect
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Apply parallax effects
useEffect(() => {
layers.forEach(layer => {
const mouseOffset = {
x: mousePosition.x * layer.depth * mouseInfluence * 10,
y: mousePosition.y * layer.depth * mouseInfluence * 10
};
const scrollOffset = scrollPosition * layer.depth * scrollInfluence * 0.1;
const totalElevation = mouseOffset.y + scrollOffset;
zSpaceAnimation.animateToZSpace(layer.id, totalElevation, {
duration: 50 // Quick response for parallax
});
});
}, [layers, mousePosition, scrollPosition, mouseInfluence, scrollInfluence, zSpaceAnimation]);
return {
...zSpaceAnimation,
mousePosition,
scrollPosition
};
}
// Hook for 3D card stacking effects
function useCardStackZSpace(cardCount, config = {}) {
const {
cardSpacing = 2,
maxRotation = 15,
interactionRadius = 200,
...zSpaceConfig
} = config;
const [hoveredCard, setHoveredCard] = useState(null);
const [mousePosition, setMousePosition] = useState({
x: 0,
y: 0
});
// Create card layers
const cardLayers = Array.from({
length: cardCount
}, (_, index) => ({
id: `card-${index}`,
zIndex: cardCount - index,
elevation: index * cardSpacing,
opacity: 1,
scale: 1,
blur: 0,
transform: `translateZ(${index * cardSpacing}px)`
}));
const zSpaceAnimation = useZSpaceAnimation(cardLayers, zSpaceConfig);
// Mouse interaction for 3D effects
useEffect(() => {
const handleMouseMove = event => {
setMousePosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Apply 3D card effects
useEffect(() => {
cardLayers.forEach((_, index) => {
const cardId = `card-${index}`;
const baseElevation = index * cardSpacing;
if (hoveredCard === index) {
// Lift hovered card
zSpaceAnimation.animateToZSpace(cardId, baseElevation + 20, {
duration: 200
});
} else if (hoveredCard !== null) {
// Adjust other cards based on proximity to hovered card
const distance = Math.abs(index - hoveredCard);
const elevationOffset = Math.max(0, 10 - distance * 2);
zSpaceAnimation.animateToZSpace(cardId, baseElevation + elevationOffset, {
duration: 300
});
} else {
// Return to base position
zSpaceAnimation.animateToZSpace(cardId, baseElevation, {
duration: 400
});
}
});
}, [cardLayers, hoveredCard, cardSpacing, zSpaceAnimation]);
const handleCardHover = useCallback(cardIndex => {
setHoveredCard(cardIndex);
}, []);
const handleCardLeave = useCallback(() => {
setHoveredCard(null);
}, []);
return {
...zSpaceAnimation,
hoveredCard,
handleCardHover,
handleCardLeave,
cardCount
};
}
// Hook for depth-based navigation
function useDepthNavigation(navigationItems, config = {}) {
const [activeItem, setActiveItem] = useState(null);
const [navigationPath, setNavigationPath] = useState([]);
// Create navigation layers with depth-based positioning
const navLayers = navigationItems.map((item, index) => ({
id: item?.id,
zIndex: 10 - item?.depth,
elevation: item?.depth * 5,
opacity: 1,
scale: 1 - item?.depth * 0.1,
blur: item?.depth * 2,
transform: `scale(${1 - item?.depth * 0.1}) translateZ(${item?.depth * 5}px)`
}));
const zSpaceAnimation = useZSpaceAnimation(navLayers, config);
const navigateTo = useCallback(itemId => {
const item = navigationItems.find(i => i.id === itemId);
if (!item) return;
// Update navigation path
const newPath = [...navigationPath];
const existingIndex = newPath.indexOf(itemId);
if (existingIndex >= 0) {
// Going back in navigation
newPath.splice(existingIndex + 1);
} else {
// Going deeper
newPath.push(itemId);
}
setNavigationPath(newPath);
setActiveItem(itemId);
// Animate depth changes
navigationItems.forEach(navItem => {
const isInPath = newPath.includes(navItem.id);
const isActive = navItem.id === itemId;
const targetElevation = isActive ? 15 : isInPath ? 10 - newPath.indexOf(navItem.id) * 2 : navItem.depth * 5;
zSpaceAnimation.animateToZSpace(navItem.id, targetElevation, {
duration: 300
});
});
}, [navigationItems, navigationPath, zSpaceAnimation]);
const goBack = useCallback(() => {
if ((navigationPath?.length || 0) > 1) {
const previousItem = navigationPath[(navigationPath?.length || 0) - 2];
navigateTo(previousItem);
}
}, [navigationPath, navigateTo]);
const goHome = useCallback(() => {
if ((navigationPath?.length || 0) > 0) {
navigateTo(navigationPath[0]);
}
}, [navigationPath, navigateTo]);
return {
...zSpaceAnimation,
activeItem,
navigationPath,
navigateTo,
goBack,
goHome,
canGoBack: (navigationPath?.length || 0) > 1
};
}
export { useCardStackZSpace, useDepthNavigation, useParallaxZSpace, useZSpaceAnimation };
//# sourceMappingURL=useZSpaceAnimation.js.map