UNPKG

aura-glass

Version:

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

497 lines (479 loc) 16.9 kB
'use client'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { Canvas, useThree, useFrame } from '@react-three/fiber'; import { AnimatePresence, motion } from 'framer-motion'; import { RotateCcw, Zap, Triangle } from 'lucide-react'; import { useRef, useState, useEffect, useCallback } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import * as THREE from 'three'; import { SeededRandom } from '../../utils/random.js'; // Shatter geometry factory const ShatterGeometryFactory = { createGlassShard: (size = 1, complexity = 3, random = new SeededRandom()) => { const geometry = new THREE.PlaneGeometry(size, size, complexity, complexity); // Add random jagged edges const positions = geometry.attributes.position.array; for (let i = 0; i < positions.length; i += 3) { const x = positions[i]; const y = positions[i + 1]; // Add some randomness to edges const edgeFactor = Math.abs(x) + Math.abs(y); if (edgeFactor > 0.8) { positions[i] += random.nextSigned() * 0.1; positions[i + 1] += random.nextSigned() * 0.1; positions[i + 2] += random.nextSigned() * 0.05; } } geometry.computeVertexNormals(); return geometry; }, createShatterField: (count = 20, spread = 5, random = new SeededRandom()) => { const geometries = []; for (let i = 0; i < count; i++) { const size = 0.5 + random.next() * 1.5; const complexity = 2 + random.nextInt(0, 2); const geometry = ShatterGeometryFactory.createGlassShard(size, complexity, random); geometries.push(geometry); } return geometries; } }; // Shatter material factory const ShatterMaterialFactory = { createGlassShardMaterial: (options = {}) => { const { color = new THREE.Color(0.7, 0.9, 1.0), opacity = 0.8, refractionRatio = 1.5, reflectivity = 0.8 } = options; return new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, color: { value: color }, opacity: { value: opacity }, refractionRatio: { value: refractionRatio }, reflectivity: { value: reflectivity }, shatterProgress: { value: 0 } }, vertexShader: ` varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUv; void main() { vPosition = position; vNormal = normal; vUv = uv; // Add shatter displacement vec3 displacedPosition = position; displacedPosition += normal * sin(time * 10.0 + position.x * 5.0) * 0.02; gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0); } `, fragmentShader: ` uniform float time; uniform vec3 color; uniform float opacity; uniform float refractionRatio; uniform float reflectivity; uniform float shatterProgress; varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUv; void main() { // Fresnel effect for glass-like appearance float fresnel = pow(1.0 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0); // Add some color variation based on position vec3 finalColor = color; finalColor += vec3(sin(vPosition.x * 10.0) * 0.1, cos(vPosition.y * 10.0) * 0.1, sin(vPosition.z * 10.0) * 0.1); // Shatter effect float shatterEffect = sin(time * 20.0 + vPosition.x * 5.0 + vPosition.y * 5.0) * 0.5 + 0.5; finalColor *= (1.0 - shatterProgress * 0.3); float finalOpacity = opacity * (0.7 + 0.3 * fresnel) * (1.0 - shatterProgress * 0.2); gl_FragColor = vec4(finalColor, finalOpacity); } `, transparent: true, side: THREE.DoubleSide }); }, createRefractionMaterial: (options = {}) => { const { color = new THREE.Color(0.5, 0.8, 1.0), intensity = 1.0 } = options; return new THREE.ShaderMaterial({ uniforms: { time: { value: 0 }, color: { value: color }, intensity: { value: intensity }, refractionStrength: { value: 0.1 } }, vertexShader: ` varying vec3 vPosition; varying vec3 vNormal; void main() { vPosition = position; vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float time; uniform vec3 color; uniform float intensity; uniform float refractionStrength; varying vec3 vPosition; varying vec3 vNormal; void main() { // Simulate refraction through glass vec3 refractedColor = color; refractedColor.r += sin(vPosition.x * 20.0 + time * 5.0) * refractionStrength; refractedColor.g += cos(vPosition.y * 20.0 + time * 5.0) * refractionStrength; refractedColor.b += sin(vPosition.z * 20.0 + time * 5.0) * refractionStrength; // Add some sparkle effect float sparkle = sin(vPosition.x * 50.0 + time * 10.0) * cos(vPosition.y * 50.0 + time * 10.0) * 0.5 + 0.5; refractedColor += sparkle * intensity * 0.3; gl_FragColor = vec4(refractedColor, 0.8); } `, transparent: true }); } }; // Shatter animation system const ShatterAnimations = { createShardAnimation: (shard, targetPosition, targetRotation, duration = 2) => { const startPosition = shard.position.clone(); const startRotation = shard.rotation.clone(); const startTime = Date.now(); return () => { const elapsed = (Date.now() - startTime) / 1000; const progress = Math.min(elapsed / duration, 1); // Easing function for smooth animation const easeOut = t => 1 - Math.pow(1 - t, 3); const easedProgress = easeOut(progress); // Interpolate position shard.position.lerpVectors(startPosition, targetPosition, easedProgress); // Interpolate rotation shard.rotation.x = THREE.MathUtils.lerp(startRotation.x, targetRotation.x, easedProgress); shard.rotation.y = THREE.MathUtils.lerp(startRotation.y, targetRotation.y, easedProgress); shard.rotation.z = THREE.MathUtils.lerp(startRotation.z, targetRotation.z, easedProgress); return progress >= 1; }; }, createExplosionAnimation: (shards, center, force = 10, duration = 1) => { useReducedMotion(); const animations = shards.map((shard, index) => { const direction = new THREE.Vector3((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2).normalize(); const distance = 2 + Math.random() * 3; const targetPosition = center.clone().add(direction.multiplyScalar(distance)); const targetRotation = new THREE.Euler(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2); return ShatterAnimations.createShardAnimation(shard, targetPosition, targetRotation, duration); }); return () => { const allComplete = animations.every(animation => animation()); return allComplete; }; }, createReformAnimation: (shards, originalPositions, duration = 2) => { const animations = shards.map((shard, index) => { const targetPosition = originalPositions[index]; const targetRotation = new THREE.Euler(0, 0, 0); return ShatterAnimations.createShardAnimation(shard, targetPosition, targetRotation, duration); }); return () => { const allComplete = animations.every(animation => animation()); return allComplete; }; } }; function GlassShatterEffects({ children, className = "", trigger = "click", duration = 2, intensity = 1, shardCount = 12, autoReform = true, reformDelay = 3000, onShatter, onReform, disabled = false, showControls = false, seed }) { const prefersReducedMotion = useReducedMotion(); const containerRef = useRef(null); const canvasRef = useRef(null); const [isShattered, setIsShattered] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [shards, setShards] = useState([]); const [originalPositions, setOriginalPositions] = useState([]); const [currentAnimation, setCurrentAnimation] = useState(null); // Initialize shards useEffect(() => { if (!canvasRef.current) return; const seededRandom = new SeededRandom(seed ?? `${shardCount}`); const geometries = ShatterGeometryFactory.createShatterField(shardCount, 5, seededRandom); const newShards = []; const positions = []; geometries.forEach((geometry, index) => { const material = ShatterMaterialFactory.createGlassShardMaterial({ opacity: 0.8 - seededRandom.next() * 0.3, refractionRatio: 1.3 + seededRandom.next() * 0.4 }); const shard = new THREE.Mesh(geometry, material); shard.position.set(seededRandom.nextSigned() * 2, seededRandom.nextSigned() * 2, 0); shard.rotation.set(seededRandom.next() * Math.PI * 2, seededRandom.next() * Math.PI * 2, seededRandom.next() * Math.PI * 2); newShards.push(shard); positions.push(shard.position.clone()); }); setShards(newShards); setOriginalPositions(positions); }, [shardCount, seed]); // Trigger shatter effect const triggerShatter = useCallback(() => { if (disabled || isAnimating) return; setIsShattered(true); setIsAnimating(true); onShatter?.(); // Create explosion animation const center = new THREE.Vector3(0, 0, 0); const animation = ShatterAnimations.createExplosionAnimation(shards, center, intensity * 10, duration); setCurrentAnimation(() => animation); // Auto reform after delay if (autoReform) { setTimeout(() => { triggerReform(); }, reformDelay); } }, [disabled, isAnimating, shards, intensity, duration, autoReform, reformDelay, onShatter]); // Trigger reform effect const triggerReform = useCallback(() => { if (!isShattered || isAnimating) return; setIsAnimating(true); onReform?.(); const animation = ShatterAnimations.createReformAnimation(shards, originalPositions, duration); setCurrentAnimation(() => animation); }, [isShattered, isAnimating, shards, originalPositions, duration, onReform]); // Manual trigger const handleManualTrigger = useCallback(() => { if (isShattered) { triggerReform(); } else { triggerShatter(); } }, [isShattered, triggerReform, triggerShatter]); // Handle different trigger types useEffect(() => { if (disabled) return; const element = containerRef.current; if (!element) return; const handleClick = () => { if (trigger === "click") { handleManualTrigger(); } }; const handleMouseEnter = () => { if (trigger === "hover") { triggerShatter(); } }; const handleMouseLeave = () => { if (trigger === "hover" && autoReform) { setTimeout(() => { triggerReform(); }, reformDelay); } }; if (trigger === "click") { element.addEventListener("click", handleClick); } else if (trigger === "hover") { element.addEventListener("mouseenter", handleMouseEnter); element.addEventListener("mouseleave", handleMouseLeave); } return () => { element.removeEventListener("click", handleClick); element.removeEventListener("mouseenter", handleMouseEnter); element.removeEventListener("mouseleave", handleMouseLeave); }; }, [trigger, disabled, handleManualTrigger, triggerShatter, triggerReform, autoReform, reformDelay]); // Auto trigger effect useEffect(() => { if (trigger === "auto" && !disabled) { const interval = setInterval(() => { useReducedMotion(); if (!isAnimating) { triggerShatter(); } }, 5000); return () => clearInterval(interval); } }, [trigger, disabled, isAnimating, triggerShatter]); return jsxs("div", { ref: containerRef, className: cn("glass-shatter-effects glass-relative glass-overflow-hidden", className), style: { position: "relative", cursor: trigger === "click" ? "pointer" : "default" }, children: [jsx("div", { className: cn("content glass-transition-opacity glass-duration-300", isShattered ? "glass-opacity-0" : "glass-opacity-100"), children: children }), jsx(AnimatePresence, { children: isShattered && jsx(motion.div, { initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, className: cn("glass-absolute glass-inset-0 glass-pointer-events-none"), children: jsx(Canvas, { ref: canvasRef, className: cn("glass-w-full glass-h-full"), camera: { position: [0, 0, 5], fov: 75 }, gl: { alpha: true, antialias: true }, children: jsx(ShatterScene, { shards: shards, currentAnimation: currentAnimation, setCurrentAnimation: setCurrentAnimation, setIsAnimating: setIsAnimating, setIsShattered: setIsShattered, isShattered: isShattered }) }) }) }), showControls && jsxs(motion.div, { initial: { opacity: 0, y: 20 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, className: cn("glass-absolute glass-bottom-4 glass-right-4 glass-flex glass-gap-2"), children: [jsx("button", { onClick: handleManualTrigger, disabled: isAnimating, className: cn("glass-p-2 glass-surface-subtle glass-foundation-complete glass-radius-lg glass-border glass-border-primary hover:glass-surface-hover glass-transition-colors disabled:glass-opacity-50 glass-focus glass-touch-target glass-contrast-guard"), title: isShattered ? "Reform" : "Shatter", children: isShattered ? jsx(RotateCcw, { className: cn("glass-w-4 glass-h-4") }) : jsx(Zap, { className: cn("glass-w-4 glass-h-4") }) }), isAnimating && jsxs("div", { className: cn("glass-flex glass-items-center glass-gap-2 glass-px-3 glass-py-2 glass-surface-info glass-foundation-complete glass-radius-lg glass-text-info glass-text-sm"), children: [jsx(motion.div, { animate: prefersReducedMotion ? {} : { rotate: 360 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 1, repeat: Infinity, ease: "linear" }, children: jsx(Triangle, { className: cn("glass-w-3 glass-h-3") }) }), "Animating..."] })] })] }); } // 3D Shatter Scene Component function ShatterScene({ shards, currentAnimation, setCurrentAnimation, setIsAnimating, setIsShattered, isShattered }) { const { scene } = useThree(); // Add shards to scene useEffect(() => { shards.forEach(shard => { scene.add(shard); }); return () => { shards.forEach(shard => { scene.remove(shard); }); }; }, [shards, scene]); // Animation loop useFrame(state => { const time = state.clock.elapsedTime; // Update shard materials shards.forEach((shard, index) => { if (shard.material && "uniforms" in shard.material && shard.material.uniforms) { if (typeof shard.material.uniforms === "object" && "time" in shard.material.uniforms) shard.material.uniforms.time.value = time; if (typeof shard.material.uniforms === "object" && "shatterProgress" in shard.material.uniforms) shard.material.uniforms.shatterProgress.value = isShattered ? 1 : 0; } }); // Run current animation if (currentAnimation) { const complete = currentAnimation(); if (complete) { setCurrentAnimation(null); setIsAnimating(false); if (!isShattered) { setIsShattered(false); } } } }); return jsxs(Fragment, { children: [jsx("ambientLight", { intensity: 0.4 }), jsx("pointLight", { position: [10, 10, 10], intensity: 0.8 }), jsx("pointLight", { position: [-10, -10, -10], intensity: 0.3, color: 0x4488ff })] }); } export { GlassShatterEffects, GlassShatterEffects as default }; //# sourceMappingURL=GlassShatterEffects.js.map