UNPKG

aura-glass

Version:

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

519 lines (516 loc) 16.2 kB
'use client'; import { jsxs } from 'react/jsx-runtime'; import { useRef, useState, useCallback, useEffect, useMemo } from 'react'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useAnimation, useSpring, motion } from 'framer-motion'; // Default physics properties const DEFAULT_PHYSICS = { mass: 1, tension: 300, friction: 30, velocity: 0, acceleration: 0, gravity: 0.5, elasticity: 0.8, viscosity: 0.1 }; // Emotional context animation mappings const EMOTIONAL_ANIMATIONS = { calm: { duration: 3, ease: [0.25, 0.46, 0.45, 0.94], // Calm bezier curve type: "spring", stiffness: 50, damping: 20 }, excited: { duration: 0.8, ease: [0.68, -0.55, 0.265, 1.55], // Excited bounce type: "spring", stiffness: 400, damping: 15 }, focused: { duration: 2, ease: [0.4, 0, 0.2, 1], // Linear focus type: "tween" }, stressed: { duration: 0.5, ease: [0.76, 0, 0.24, 1], // Sharp stressed motion type: "spring", stiffness: 500, damping: 25 }, joyful: { duration: 1.2, ease: [0.175, 0.885, 0.32, 1.275], // Joyful bounce type: "spring", stiffness: 200, damping: 10 }, melancholy: { duration: 4, ease: [0.77, 0, 0.175, 1], // Slow melancholy type: "tween" }, energetic: { duration: 0.6, ease: [0.23, 1, 0.32, 1], // Quick energy type: "spring", stiffness: 350, damping: 12 }, peaceful: { duration: 5, ease: [0.25, 0.46, 0.45, 0.94], // Very calm type: "spring", stiffness: 30, damping: 25 }, urgent: { duration: 0.3, ease: [0.55, 0.055, 0.675, 0.19], // Sharp urgent type: "tween" }, contemplative: { duration: 6, ease: [0.19, 1, 0.22, 1], // Slow contemplation type: "spring", stiffness: 40, damping: 30 } }; // Organic motion pattern definitions const MOTION_PATTERNS = { breathe: { animate: { scale: [1, 1.02, 1], opacity: [0.8, 1, 0.8] } }, pulse: { animate: { scale: [1, 1.05, 1], boxShadow: ["0 0 0 0 var(--glass-bg-hover)", "0 0 0 20px rgba(255, 255, 255, 0)", "0 0 0 0 rgba(255, 255, 255, 0)"] } }, wave: { animate: { y: [0, -10, 0, 10, 0], rotateX: [0, 5, 0, -5, 0] } }, flutter: { animate: { x: [0, 2, -2, 1, -1, 0], y: [0, -1, 1, -0.5, 0.5, 0], rotate: [0, 1, -1, 0.5, -0.5, 0] } }, drift: { animate: { x: [0, 20, -20, 15, -15, 0], y: [0, -5, 5, -3, 3, 0], rotate: [0, 2, -2, 1, -1, 0] } }, bloom: { animate: { scale: [0.8, 1.2, 1], opacity: [0.5, 1, 1], filter: ["blur(var(--glass-glass-blur-md)) brightness(0.8)", "blur(0px) brightness(1.2)", "blur(0px) brightness(1)"] } }, cascade: { animate: { y: [0, 0, 0, 0, 0], opacity: [0, 1, 1, 1, 0], scale: [0.9, 1, 1, 1, 0.9] } }, ripple: { animate: { scale: [1, 1.1, 1.05, 1], borderRadius: ["10px", "50%", "10px", "10px"] } }, shimmer: { animate: { background: ["linear-gradient(90deg, transparent 0%, rgba(var(--glass-color-white) / var(--glass-opacity-20)) 50%, transparent 100%)", "linear-gradient(90deg, transparent 25%, var(--glass-border-default) 75%, transparent 100%)", "linear-gradient(90deg, transparent 50%, rgba(var(--glass-color-white) / var(--glass-opacity-20)) 100%, transparent 100%)"], backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"] } }, flow: { animate: { clipPath: ["ellipse(100% 100% at 0% 50%)", "ellipse(100% 100% at 50% 50%)", "ellipse(100% 100% at 100% 50%)", "ellipse(100% 100% at 50% 50%)", "ellipse(100% 100% at 0% 50%)"] } }, crystallize: { animate: { clipPath: ["polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", "polygon(20% 0%, 80% 20%, 100% 100%, 0% 80%)", "polygon(10% 10%, 90% 0%, 90% 90%, 10% 100%)", "polygon(0% 20%, 100% 10%, 80% 100%, 20% 90%)", "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)"], filter: ["brightness(1) contrast(1)", "brightness(1.2) contrast(1.1)", "brightness(1.1) contrast(1.05)", "brightness(1.15) contrast(1.08)", "brightness(1) contrast(1)"] } }, dissolve: { animate: { opacity: [1, 0.8, 0.6, 0.8, 1], filter: ["blur(0px)", "blur(2px)", "blur(5px)", "blur(2px)", "blur(0px)"], scale: [1, 0.98, 0.95, 0.98, 1] } } }; const OrganicAnimationEngine = ({ children, className = "", sequences = [], emotionalContext = "calm", enablePhysics = true, enableAdaptiveSpeed = true, enableMicroInteractions = true, performanceLevel = "high", onAnimationStart, onAnimationComplete, onEmotionalShift, showDebugHud = false }) => { useReducedMotion(); const containerRef = useRef(null); const isMountedRef = useRef(true); const timeoutIdsRef = useRef(new Set()); const isTestEnv = typeof process !== "undefined" && process.env?.JEST_WORKER_ID !== undefined; const [activeSequences, setActiveSequences] = useState(new Set()); const [currentEmotion, setCurrentEmotion] = useState(emotionalContext); const [isVisible, setIsVisible] = useState(false); const controls = useAnimation(); const clearScheduledTasks = useCallback(() => { timeoutIdsRef.current.forEach(id => clearTimeout(id)); timeoutIdsRef.current.clear(); }, []); useEffect(() => { return () => { isMountedRef.current = false; clearScheduledTasks(); }; }, [clearScheduledTasks]); const scheduleTask = useCallback((callback, delay = 0) => { if (isTestEnv) { callback(); return; } const id = window.setTimeout(() => { timeoutIdsRef.current.delete(id); callback(); }, delay); timeoutIdsRef.current.add(id); }, [isTestEnv]); // Physics-based motion values const physicsX = useSpring(0, { stiffness: DEFAULT_PHYSICS.tension, damping: DEFAULT_PHYSICS.friction }); const physicsY = useSpring(0, { stiffness: DEFAULT_PHYSICS.tension, damping: DEFAULT_PHYSICS.friction }); const physicsRotate = useSpring(0, { stiffness: DEFAULT_PHYSICS.tension * 0.5, damping: DEFAULT_PHYSICS.friction }); const physicsScale = useSpring(1, { stiffness: DEFAULT_PHYSICS.tension, damping: DEFAULT_PHYSICS.friction }); // Performance optimization based on level const optimizeForPerformance = useMemo(() => { switch (performanceLevel) { case "low": return { maxSequences: 2, reducedMotion: true, skipComplexAnimations: true }; case "medium": return { maxSequences: 4, reducedMotion: false, skipComplexAnimations: false }; case "high": return { maxSequences: 8, reducedMotion: false, skipComplexAnimations: false }; case "ultra": return { maxSequences: 16, reducedMotion: false, skipComplexAnimations: false }; default: return { maxSequences: 8, reducedMotion: false, skipComplexAnimations: false }; } }, [performanceLevel]); // Adaptive speed calculation based on device performance const adaptiveSpeedFactor = useMemo(() => { if (!enableAdaptiveSpeed) return 1; // Simple performance detection (in a real app, would use more sophisticated metrics) const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : ""; const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); const isLowEnd = /Android.*Chrome\/[1-6][0-9]\./.test(userAgent); if (isLowEnd) return 0.5; if (isMobile) return 0.7; return 1; }, [enableAdaptiveSpeed]); // Emotional context change handler useEffect(() => { if (currentEmotion !== emotionalContext) { onEmotionalShift?.(currentEmotion, emotionalContext); setCurrentEmotion(emotionalContext); } }, [emotionalContext, currentEmotion, onEmotionalShift]); // Visibility observer for performance optimization useEffect(() => { if (isTestEnv) { setIsVisible(true); return; } if (!containerRef.current) return; const observer = new IntersectionObserver(([entry]) => { setIsVisible(entry.isIntersecting); }, { threshold: 0.1 }); observer.observe(containerRef.current); return () => observer.disconnect(); }, []); // Execute animation sequence const executeSequence = useCallback(async sequence => { if (!isVisible && sequence.trigger !== "auto") return; if (activeSequences.size >= optimizeForPerformance.maxSequences) return; if (optimizeForPerformance.skipComplexAnimations && ["crystallize", "dissolve", "flow"].includes(sequence.pattern)) return; if (isTestEnv || !isMountedRef.current) return; setActiveSequences(prev => new Set([...prev, sequence.id])); onAnimationStart?.(sequence.id); try { const emotionalTransition = EMOTIONAL_ANIMATIONS[sequence.emotional] || EMOTIONAL_ANIMATIONS[currentEmotion]; const motionPattern = MOTION_PATTERNS[sequence.pattern]; // Apply adaptive speed const adaptedDuration = (sequence.duration || emotionalTransition.duration || 2) * adaptiveSpeedFactor; // Merge physics properties if enabled let finalTransition = { ...emotionalTransition, duration: adaptedDuration }; if (enablePhysics && sequence.physics) { finalTransition = { ...finalTransition, type: "spring", stiffness: sequence.physics.tension || DEFAULT_PHYSICS.tension, damping: sequence.physics.friction || DEFAULT_PHYSICS.friction }; } // Execute the animation await controls.start({ ...motionPattern.animate, transition: finalTransition }); // Handle looping if (sequence.loop === true && !isTestEnv) { // Infinite loop - restart the sequence scheduleTask(() => executeSequence(sequence), sequence.delay || 0); } else if (typeof sequence.loop === "number" && sequence.loop > 1 && !isTestEnv) { // Finite loop - decrement and restart const remainingLoops = sequence.loop - 1; const loopingSequence = { ...sequence, loop: remainingLoops }; scheduleTask(() => executeSequence(loopingSequence), sequence.delay || 0); } } catch (error) { console.warn("Animation sequence failed:", error); } finally { if (isMountedRef.current) { setActiveSequences(prev => { const next = new Set(prev); next.delete(sequence.id); return next; }); onAnimationComplete?.(sequence.id); } } }, [isVisible, activeSequences.size, optimizeForPerformance, currentEmotion, adaptiveSpeedFactor, enablePhysics, controls, onAnimationStart, onAnimationComplete]); // Auto-trigger sequences useEffect(() => { sequences.filter(seq => seq.trigger === "auto").forEach(sequence => { if (isTestEnv) return; const delay = sequence.delay || 0; scheduleTask(() => executeSequence(sequence), delay); }); }, [sequences, executeSequence]); // Micro-interaction handlers const handleHover = useCallback(() => { if (!enableMicroInteractions) return; const hoverSequences = sequences.filter(seq => seq.trigger === "hover"); hoverSequences.forEach(executeSequence); // Physics-based hover effect if (enablePhysics) { physicsScale.set(1.02); physicsY.set(-2); } }, [enableMicroInteractions, sequences, executeSequence, enablePhysics, physicsScale, physicsY]); const handleHoverEnd = useCallback(() => { if (!enableMicroInteractions || !enablePhysics) return; physicsScale.set(1); physicsY.set(0); }, [enableMicroInteractions, enablePhysics, physicsScale, physicsY]); const handleClick = useCallback(event => { if (!enableMicroInteractions) return; const clickSequences = sequences.filter(seq => seq.trigger === "click"); clickSequences.forEach(executeSequence); // Physics-based click effect if (enablePhysics) { const bounds = containerRef.current?.getBoundingClientRect(); if (bounds) { const centerX = bounds.width / 2; const centerY = bounds.height / 2; const clickX = event.clientX - bounds.left; const clickY = event.clientY - bounds.top; const offsetX = (clickX - centerX) / centerX; const offsetY = (clickY - centerY) / centerY; physicsX.set(offsetX * 10); physicsY.set(offsetY * 10); physicsRotate.set(offsetX * 2); physicsScale.set(0.98); // Reset after animation scheduleTask(() => { physicsX.set(0); physicsY.set(0); physicsRotate.set(0); physicsScale.set(1); }, 200); } } }, [enableMicroInteractions, sequences, executeSequence, enablePhysics, physicsX, physicsY, physicsRotate, physicsScale]); const handleFocus = useCallback(() => { if (!enableMicroInteractions) return; const focusSequences = sequences.filter(seq => seq.trigger === "focus"); focusSequences.forEach(executeSequence); }, [enableMicroInteractions, sequences, executeSequence]); // Scroll-based animations useEffect(() => { const handleScroll = () => { if (!enableMicroInteractions) return; const scrollSequences = sequences.filter(seq => seq.trigger === "scroll"); scrollSequences.forEach(executeSequence); }; if (enableMicroInteractions && !isTestEnv) { window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); } }, [enableMicroInteractions, sequences, executeSequence, isTestEnv]); return jsxs(motion.div, { ref: containerRef, className: `organic-animation-container ${className}`, style: { position: "relative", x: enablePhysics ? physicsX : 0, y: enablePhysics ? physicsY : 0, rotate: enablePhysics ? physicsRotate : 0, scale: enablePhysics ? physicsScale : 1 }, animate: controls, onMouseEnter: handleHover, onMouseLeave: handleHoverEnd, onClick: handleClick, onFocus: handleFocus, whileTap: enableMicroInteractions ? { scale: 0.98 } : undefined, children: [children, showDebugHud && jsxs("div", { className: 'animation-debug-info glass-focus glass-touch-target glass-contrast-guard', style: { position: "absolute", top: 0, right: 0, background: '/* Use createGlassStyle({ intent: "primary", elevation: "level2" }) */', color: "white", padding: "4px 8px", fontSize: "10px", borderRadius: "0 0 0 8px", pointerEvents: "none" }, children: ["Emotion: ", currentEmotion, " | Active: ", activeSequences.size, " | Perf:", " ", performanceLevel] })] }); }; // Predefined animation sequences for common use cases const COMMON_SEQUENCES = { gentle: [{ id: "gentle-breathe", pattern: "breathe", duration: 4, delay: 0, intensity: 0.3, emotional: "calm", loop: true, trigger: "auto" }], energetic: [{ id: "energetic-pulse", pattern: "pulse", duration: 1, delay: 0, intensity: 0.8, emotional: "energetic", loop: true, trigger: "auto" }], interactive: [{ id: "hover-shimmer", pattern: "shimmer", duration: 1.5, delay: 0, intensity: 0.6, emotional: "focused", trigger: "hover" }, { id: "click-ripple", pattern: "ripple", duration: 0.8, delay: 0, intensity: 1, emotional: "excited", trigger: "click" }], contemplative: [{ id: "drift-slow", pattern: "drift", duration: 8, delay: 2, intensity: 0.2, emotional: "contemplative", loop: true, trigger: "auto" }] }; export { COMMON_SEQUENCES, OrganicAnimationEngine, OrganicAnimationEngine as default }; //# sourceMappingURL=OrganicAnimationEngine.js.map