aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
352 lines (349 loc) • 11.5 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import { useRef, useState, useCallback, useEffect } from 'react';
import { cn } from '../../lib/utilsComprehensive.js';
import { useAnimation, useMotionValue, useSpring, useTransform, motion } from 'framer-motion';
const defaultPhysics = {
gravity: 0.5,
friction: 0.98,
elasticity: 0.7,
viscosity: 0.1,
refractionIndex: 1.5,
density: 2.5
};
const GlassPhysicsEngine = ({
children,
className = '',
interaction = 'ripple',
physics = {},
enabled = true,
intensity = 1,
autoTrigger = false,
triggerDelay = 2000,
onInteractionStart,
onInteractionEnd
}) => {
useReducedMotion();
const containerRef = useRef(null);
const canvasRef = useRef(null);
const animationFrameRef = useRef();
const [particles, setParticles] = useState([]);
const [isAnimating, setIsAnimating] = useState(false);
const controls = useAnimation();
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useSpring(0, {
stiffness: 300,
damping: 30
});
const rotateY = useSpring(0, {
stiffness: 300,
damping: 30
});
const physicsConfig = {
...defaultPhysics,
...physics
};
// Transform mouse position to rotation
const rotateXTransform = useTransform(y, [-300, 300], [15, -15]);
const rotateYTransform = useTransform(x, [-300, 300], [-15, 15]);
// Create particle system
const createParticles = useCallback((centerX, centerY, count, type) => {
const newParticles = [];
for (let i = 0; i < count; i++) {
const angle = Math.PI * 2 * i / count + Math.random() * 0.5;
const velocity = 2 + Math.random() * 4 * intensity;
const life = 60 + Math.random() * 120;
newParticles.push({
id: `particle-${Date.now()}-${i}`,
x: centerX,
y: centerY,
vx: Math.cos(angle) * velocity,
vy: Math.sin(angle) * velocity,
life: life,
maxLife: life,
size: 2 + Math.random() * 4,
opacity: 1,
color: `hsl(${180 + Math.random() * 60}, 70%, ${50 + Math.random() * 30}%)`,
type
});
}
return newParticles;
}, [intensity]);
// Update particle physics
const updateParticles = useCallback(particles => {
return particles.map(particle => {
// Apply gravity and friction
particle.vy += physicsConfig.gravity;
particle.vx *= physicsConfig.friction;
particle.vy *= physicsConfig.friction;
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Update life
particle.life--;
particle.opacity = particle.life / particle.maxLife;
// Boundary collision with elasticity
if (containerRef.current) {
const bounds = containerRef.current.getBoundingClientRect();
if (particle.x <= 0 || particle.x >= bounds.width) {
particle.vx *= -physicsConfig.elasticity;
particle.x = Math.max(0, Math.min(bounds.width, particle.x));
}
if (particle.y <= 0 || particle.y >= bounds.height) {
particle.vy *= -physicsConfig.elasticity;
particle.y = Math.max(0, Math.min(bounds.height, particle.y));
}
}
return particle;
}).filter(particle => particle.life > 0);
}, [physicsConfig]);
// Render particles to canvas
const renderParticles = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!canvas || !ctx) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Render particles
particles.forEach(particle => {
ctx.save();
// Set particle properties
ctx.globalAlpha = particle.opacity;
ctx.fillStyle = particle.color;
ctx.shadowBlur = 10;
ctx.shadowColor = particle.color;
// Draw particle based on type
switch (particle.type) {
case 'fragment':
ctx.fillRect(particle.x - particle.size / 2, particle.y - particle.size / 2, particle.size, particle.size);
break;
case 'droplet':
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
break;
case 'spark':
ctx.strokeStyle = particle.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(particle.x - particle.vx, particle.y - particle.vy);
ctx.lineTo(particle.x, particle.y);
ctx.stroke();
break;
case 'dust':
ctx.globalAlpha = particle.opacity * 0.3;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size * 0.5, 0, Math.PI * 2);
ctx.fill();
break;
}
ctx.restore();
});
}, [particles]);
// Animation loop
const animate = useCallback(() => {
if (!isAnimating) return;
setParticles(prevParticles => {
const updated = updateParticles(prevParticles);
if (updated.length === 0) {
setIsAnimating(false);
onInteractionEnd?.(interaction);
return [];
}
return updated;
});
renderParticles();
animationFrameRef.current = requestAnimationFrame(animate);
}, [isAnimating, updateParticles, renderParticles, interaction, onInteractionEnd]);
// Start animation loop
useEffect(() => {
if (isAnimating) {
animate();
}
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isAnimating, animate]);
// Handle different interaction types
const triggerInteraction = useCallback((clientX, clientY, interactionType = interaction) => {
if (!enabled || !containerRef.current) return;
const bounds = containerRef.current.getBoundingClientRect();
const x = clientX - bounds.left;
const y = clientY - bounds.top;
onInteractionStart?.(interactionType);
switch (interactionType) {
case 'ripple':
// Create ripple effect
controls.start({
scale: [1, 1.05, 1],
transition: {
duration: 0.8,
ease: 'easeOut'
}
});
// Add ripple particles
const rippleParticles = createParticles(x, y, 20 * intensity, 'droplet');
setParticles(rippleParticles);
setIsAnimating(true);
break;
case 'shatter':
// Create shatter effect
controls.start({
scale: [1, 0.95, 1.02, 1],
rotateX: [0, 2, -1, 0],
rotateY: [0, -1, 2, 0],
transition: {
duration: 1.2,
ease: 'easeOut'
}
});
// Add glass fragments
const fragments = createParticles(x, y, 50 * intensity, 'fragment');
setParticles(fragments);
setIsAnimating(true);
break;
case 'bend':
// Create bending effect
const bendX = (x - bounds.width / 2) / bounds.width * 20;
const bendY = (y - bounds.height / 2) / bounds.height * 20;
controls.start({
rotateX: [0, bendY, 0],
rotateY: [0, bendX, 0],
transition: {
duration: 1.5,
ease: 'easeInOut'
}
});
break;
case 'melt':
// Create melting effect
controls.start({
scaleY: [1, 0.8, 1],
y: [0, 10, 0],
transition: {
duration: 2,
ease: 'easeInOut'
}
});
// Add droplet particles
const droplets = createParticles(x, y, 15 * intensity, 'droplet');
setParticles(droplets);
setIsAnimating(true);
break;
case 'freeze':
// Create freezing effect
controls.start({
scale: [1, 0.98, 1.01, 1],
transition: {
duration: 1,
ease: 'easeOut'
}
});
// Add frost particles
const frost = createParticles(x, y, 30 * intensity, 'dust');
setParticles(frost);
setIsAnimating(true);
break;
case 'vibrate':
// Create vibration effect
controls.start({
x: [0, 2, -2, 1, -1, 0],
y: [0, 1, -1, 2, -2, 0],
transition: {
duration: 0.5,
ease: 'easeOut'
}
});
// Add vibration sparks
const sparks = createParticles(x, y, 10 * intensity, 'spark');
setParticles(sparks);
setIsAnimating(true);
break;
}
}, [enabled, interaction, intensity, controls, createParticles, onInteractionStart]);
// Handle mouse interactions
const handleMouseMove = useCallback(event => {
if (!containerRef.current) return;
const bounds = containerRef.current.getBoundingClientRect();
const mouseX = event.clientX - bounds.left - bounds.width / 2;
const mouseY = event.clientY - bounds.top - bounds.height / 2;
x.set(mouseX);
y.set(mouseY);
// Update rotation based on mouse position
rotateX.set(rotateXTransform.get());
rotateY.set(rotateYTransform.get());
}, [x, y, rotateX, rotateY, rotateXTransform, rotateYTransform]);
const handleClick = useCallback(event => {
triggerInteraction(event.clientX, event.clientY);
}, [triggerInteraction]);
// Auto-trigger effect
useEffect(() => {
if (autoTrigger && enabled) {
const timer = setTimeout(() => {
if (containerRef.current) {
const bounds = containerRef.current.getBoundingClientRect();
triggerInteraction(bounds.left + bounds.width / 2, bounds.top + bounds.height / 2);
}
}, triggerDelay);
return () => clearTimeout(timer);
}
}, [autoTrigger, enabled, triggerDelay, triggerInteraction]);
// Setup canvas
useEffect(() => {
if (canvasRef.current && containerRef.current) {
const canvas = canvasRef.current;
const container = containerRef.current;
const resizeCanvas = () => {
const bounds = container.getBoundingClientRect();
canvas.width = bounds.width;
canvas.height = bounds.height;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => window.removeEventListener('resize', resizeCanvas);
}
}, []);
return jsxs(motion.div, {
ref: containerRef,
className: cn("glass-relative glass-overflow-hidden", className),
style: {
perspective: '1000px',
transformStyle: 'preserve-3d'
},
animate: controls,
onMouseMove: handleMouseMove,
onClick: handleClick,
whileHover: {
scale: 1.02
},
whileTap: {
scale: 0.98
},
children: [jsx("canvas", {
ref: canvasRef,
className: cn("glass-absolute glass-inset-0 glass-pointer-events-none glass-z-10"),
style: {
mixBlendMode: 'screen'
}
}), jsx(motion.div, {
style: {
rotateX,
rotateY,
transformStyle: 'preserve-3d'
},
className: cn("glass-relative glass-z-0"),
children: children
}), isAnimating && jsx("div", {
className: cn("glass-absolute glass-inset-0 glass-pointer-events-none glass-z-20"),
children: jsx("div", {
className: cn("glass-absolute glass-inset-0 glass-bg-gradient-radial glass-from-white/10 glass-via-transparent glass-to-transparent glass-opacity-50 glass-animate-pulse")
})
})]
});
};
export { GlassPhysicsEngine, GlassPhysicsEngine as default };
//# sourceMappingURL=GlassPhysicsEngine.js.map