/** * motion_interactive.js * * A comprehensive solution for implementing motion-based interactive elements * with behavioral patterns for the MorphGuard application. */ class MotionInteractive { /** * Creates a new motion interactive handler * @param {string|Element} selector - CSS selector or DOM element * @param {Object} options - Configuration options */ constructor(selector, options = {}) { // Handle both string selectors and direct element references this.element = typeof selector === 'string' ? document.querySelector(selector) : selector; if (!this.element) { console.error(`Element not found: ${selector}`); return; } // Default configuration this.config = { // Animation types animationType: 'fade', // 'fade', 'slide', 'scale', 'rotate', 'custom' // General animation parameters duration: 300, easing: 'ease-in-out', delay: 0, // Behavioral patterns behavior: { type: 'hover', // 'hover', 'click', 'scroll', 'inview', 'sequence', 'auto' repeat: false, interval: 3000, // For auto behavior threshold: 0.5, // For inview behavior (0-1) pauseOnHover: true }, // Interaction feedback feedback: { sound: false, soundSrc: '', haptic: false, visual: true, cursor: false, customCursor: '' }, // State management states: { initial: 'default', transitions: { // Example: default -> hover -> active -> default 'default': { next: 'hover' }, 'hover': { next: 'active' }, 'active': { next: 'default' } } }, // Advanced motion parameters motion: { path: null, // SVG path for motion along path physics: { enabled: false, gravity: 9.8, friction: 0.95, bounce: 0.8 }, transform: { translateX: 0, translateY: 0, translateZ: 0, rotateX: 0, rotateY: 0, rotateZ: 0, scaleX: 1, scaleY: 1, scaleZ: 1 } }, // Responsiveness responsive: { enabled: true, breakpoints: { small: { maxWidth: 576, settings: { duration: 200 } }, medium: { maxWidth: 768, settings: { duration: 250 } } } }, // Performance options performance: { useGPU: true, reduceMotion: false, // Respect user preference throttle: { enabled: true, fps: 60 } } }; // Merge user options with defaults this.config = this._mergeOptions(this.config, options); // State tracking this.state = { currentState: this.config.states.initial, isAnimating: false, lastFrameTime: 0, scrollY: 0, mousePosition: { x: 0, y: 0 }, touchPosition: { x: 0, y: 0 }, isInView: false, animationFrame: null, observers: {} }; // Store original styles this.originalStyles = { transform: this.element.style.transform || '', transition: this.element.style.transition || '', opacity: this.element.style.opacity || '1' }; // Initialize interactive element this._init(); } /** * Merges default options with user-provided options * @param {Object} defaults - Default options * @param {Object} userOptions - User-provided options * @returns {Object} - Merged options * @private */ _mergeOptions(defaults, userOptions) { const result = { ...defaults }; for (const key in userOptions) { if (userOptions.hasOwnProperty(key)) { if (typeof userOptions[key] === 'object' && !Array.isArray(userOptions[key]) && result[key] && typeof result[key] === 'object') { result[key] = this._mergeOptions(result[key], userOptions[key]); } else { result[key] = userOptions[key]; } } } return result; } /** * Initialize motion interactive element * @private */ _init() { // Add base class this.element.classList.add('motion-interactive'); // Apply initial state class this._applyStateClass(this.state.currentState); // Check if reduced motion is preferred this._checkReducedMotionPreference(); // Set up responsive behavior if (this.config.responsive.enabled) { this._setupResponsiveBehavior(); } // Add interactive behaviors based on configuration this._setupBehavior(); // Set up intersection observer for in-view detection this._setupInViewDetection(); // Apply GPU acceleration if enabled if (this.config.performance.useGPU) { this._applyGPUAcceleration(); } // Setup custom cursor if enabled if (this.config.feedback.cursor && this.config.feedback.customCursor) { this._setupCustomCursor(); } } /** * Check if user prefers reduced motion * @private */ _checkReducedMotionPreference() { if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { this.config.performance.reduceMotion = true; } // Add listener for preference change window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', e => { this.config.performance.reduceMotion = e.matches; }); } /** * Apply state class to element * @param {string} state - State name * @private */ _applyStateClass(state) { // Remove all state classes this.element.classList.forEach(className => { if (className.startsWith('state-')) { this.element.classList.remove(className); } }); // Add new state class this.element.classList.add(`state-${state}`); } /** * Set up responsive behavior * @private */ _setupResponsiveBehavior() { // Apply initial responsive settings this._applyResponsiveSettings(); // Add resize listener window.addEventListener('resize', this._debounce(() => { this._applyResponsiveSettings(); }, 250)); } /** * Apply responsive settings based on current viewport * @private */ _applyResponsiveSettings() { const windowWidth = window.innerWidth; let appliedBreakpoint = null; // Check breakpoints from smallest to largest const breakpoints = this.config.responsive.breakpoints; for (const key in breakpoints) { if (breakpoints.hasOwnProperty(key)) { const breakpoint = breakpoints[key]; if (windowWidth <= breakpoint.maxWidth) { appliedBreakpoint = breakpoint; break; } } } // Apply breakpoint settings if found if (appliedBreakpoint && appliedBreakpoint.settings) { // Create a temporary config with the responsive settings const tempConfig = { ...this.config }; // Apply breakpoint settings (shallow merge) for (const key in appliedBreakpoint.settings) { if (appliedBreakpoint.settings.hasOwnProperty(key) && tempConfig.hasOwnProperty(key)) { if (typeof tempConfig[key] !== 'object' || Array.isArray(tempConfig[key])) { tempConfig[key] = appliedBreakpoint.settings[key]; } } } // Update animation parameters if needed this._updateAnimationParameters(tempConfig); } else { // Reset to default settings this._updateAnimationParameters(this.config); } } /** * Set up element behavior based on configuration * @private */ _setupBehavior() { const behaviorType = this.config.behavior.type; switch (behaviorType) { case 'hover': this._setupHoverBehavior(); break; case 'click': this._setupClickBehavior(); break; case 'scroll': this._setupScrollBehavior(); break; case 'inview': // Already set up by _setupInViewDetection break; case 'sequence': this._setupSequenceBehavior(); break; case 'auto': this._setupAutoBehavior(); break; } } /** * Set up hover behavior * @private */ _setupHoverBehavior() { this.element.addEventListener('mouseenter', () => { this._handleStateTransition(); this._animate('in'); }); this.element.addEventListener('mouseleave', () => { this._handleStateTransition(); this._animate('out'); }); } /** * Set up click behavior * @private */ _setupClickBehavior() { this.element.addEventListener('click', () => { this._handleStateTransition(); if (this.state.currentState === 'active') { this._animate('in'); } else { this._animate('out'); } // Provide haptic feedback if enabled if (this.config.feedback.haptic && navigator.vibrate) { navigator.vibrate(50); } }); } /** * Set up scroll behavior * @private */ _setupScrollBehavior() { window.addEventListener('scroll', this._throttle(() => { const scrollY = window.scrollY || window.pageYOffset; const delta = scrollY - this.state.scrollY; this.state.scrollY = scrollY; const scrollProgress = this._getScrollProgress(); this._animateWithProgress(scrollProgress); }, this.config.performance.throttle.enabled ? 1000 / this.config.performance.throttle.fps : 0)); } /** * Set up in-view detection with Intersection Observer * @private */ _setupInViewDetection() { // Skip if unsupported if (!('IntersectionObserver' in window)) return; const options = { root: null, rootMargin: '0px', threshold: this.config.behavior.threshold }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.target === this.element) { this.state.isInView = entry.isIntersecting; if (this.state.isInView) { if (this.config.behavior.type === 'inview') { this._animate('in'); } } else { if (this.config.behavior.type === 'inview') { this._animate('out'); } } } }); }, options); observer.observe(this.element); this.state.observers.inView = observer; } /** * Set up sequence behavior (animation sequence) * @private */ _setupSequenceBehavior() { // For elements that are part of a sequence // They need to be controlled externally via the API // No automatic triggers here } /** * Set up auto behavior (automatic animation) * @private */ _setupAutoBehavior() { let interval; const startAutoAnimation = () => { interval = setInterval(() => { this._handleStateTransition(); this._animate('toggle'); }, this.config.behavior.interval); }; // Start automatic animation startAutoAnimation(); // Pause on hover if configured if (this.config.behavior.pauseOnHover) { this.element.addEventListener('mouseenter', () => { clearInterval(interval); }); this.element.addEventListener('mouseleave', () => { startAutoAnimation(); }); } } /** * Set up custom cursor * @private */ _setupCustomCursor() { let cursor = document.querySelector('.custom-cursor'); // Create custom cursor if it doesn't exist if (!cursor) { cursor = document.createElement('div'); cursor.classList.add('custom-cursor'); document.body.appendChild(cursor); // Style for cursor cursor.style.position = 'fixed'; cursor.style.pointerEvents = 'none'; cursor.style.zIndex = '9999'; cursor.style.width = '20px'; cursor.style.height = '20px'; cursor.style.borderRadius = '50%'; cursor.style.backgroundColor = 'rgba(0, 0, 0, 0.2)'; cursor.style.transform = 'translate(-50%, -50%)'; cursor.style.transition = 'transform 0.1s ease-out'; cursor.style.display = 'none'; // If custom cursor is an image if (this.config.feedback.customCursor.endsWith('.png') || this.config.feedback.customCursor.endsWith('.svg') || this.config.feedback.customCursor.endsWith('.jpg')) { cursor.style.backgroundImage = `url(${this.config.feedback.customCursor})`; cursor.style.backgroundSize = 'contain'; cursor.style.backgroundRepeat = 'no-repeat'; } } // Show custom cursor when over the element this.element.addEventListener('mouseenter', () => { cursor.style.display = 'block'; document.body.style.cursor = 'none'; }); // Hide custom cursor when leaving the element this.element.addEventListener('mouseleave', () => { cursor.style.display = 'none'; document.body.style.cursor = ''; }); // Move custom cursor with mouse this.element.addEventListener('mousemove', (e) => { cursor.style.left = `${e.clientX}px`; cursor.style.top = `${e.clientY}px`; }); } /** * Handle state transition based on current state * @private */ _handleStateTransition() { const currentState = this.state.currentState; const transitions = this.config.states.transitions; if (transitions[currentState] && transitions[currentState].next) { this.state.currentState = transitions[currentState].next; this._applyStateClass(this.state.currentState); } } /** * Apply GPU acceleration to element * @private */ _applyGPUAcceleration() { // Force hardware acceleration this.element.style.willChange = 'transform'; this.element.style.transform = 'translateZ(0)'; } /** * Update animation parameters * @param {Object} config - Configuration object * @private */ _updateAnimationParameters(config) { this.element.style.transition = `all ${config.duration}ms ${config.easing} ${config.delay}ms`; } /** * Animate the element (in, out, or toggle) * @param {string} direction - Animation direction ('in', 'out', 'toggle') * @private */ _animate(direction) { // Skip animation if already animating or user prefers reduced motion if (this.state.isAnimating || this.config.performance.reduceMotion) { return; } this.state.isAnimating = true; // Play sound if enabled this._playFeedbackSound(); // Determine final state let targetState; if (direction === 'toggle') { targetState = this.element.classList.contains('animated-in') ? 'out' : 'in'; } else { targetState = direction; } // Apply animation based on type switch (this.config.animationType) { case 'fade': this._animateFade(targetState); break; case 'slide': this._animateSlide(targetState); break; case 'scale': this._animateScale(targetState); break; case 'rotate': this._animateRotate(targetState); break; case 'custom': this._animateCustom(targetState); break; } // Update animation state classes this.element.classList.remove('animated-in', 'animated-out'); this.element.classList.add(`animated-${targetState}`); // Reset animating flag after animation completes setTimeout(() => { this.state.isAnimating = false; // Trigger repeat if configured if (this.config.behavior.repeat && targetState === 'out') { setTimeout(() => { this._animate('in'); }, this.config.delay); } }, this.config.duration + this.config.delay); } /** * Animate with scroll progress (0-1) * @param {number} progress - Animation progress (0-1) * @private */ _animateWithProgress(progress) { // Clamp progress between 0 and 1 progress = Math.max(0, Math.min(1, progress)); // Apply animation based on type with progress switch (this.config.animationType) { case 'fade': this.element.style.opacity = progress; break; case 'slide': const distance = 100 * (1 - progress); this.element.style.transform = `translateY(${distance}px)`; break; case 'scale': const scale = 0.5 + (progress * 0.5); this.element.style.transform = `scale(${scale})`; break; case 'rotate': const rotation = 360 * progress; this.element.style.transform = `rotate(${rotation}deg)`; break; case 'custom': // Apply custom transform based on progress this._applyCustomTransform(progress); break; } } /** * Animate fade effect * @param {string} direction - Animation direction ('in', 'out') * @private */ _animateFade(direction) { this.element.style.opacity = direction === 'in' ? '1' : '0'; } /** * Animate slide effect * @param {string} direction - Animation direction ('in', 'out') * @private */ _animateSlide(direction) { const transform = this.config.motion.transform; const x = transform.translateX || 0; const y = transform.translateY || 0; if (direction === 'in') { this.element.style.transform = `translate(${x}px, ${y}px)`; this.element.style.opacity = '1'; } else { this.element.style.transform = `translate(${x}px, ${y + 100}px)`; this.element.style.opacity = '0'; } } /** * Animate scale effect * @param {string} direction - Animation direction ('in', 'out') * @private */ _animateScale(direction) { const transform = this.config.motion.transform; const scaleX = transform.scaleX || 1; const scaleY = transform.scaleY || 1; if (direction === 'in') { this.element.style.transform = `scale(${scaleX}, ${scaleY})`; this.element.style.opacity = '1'; } else { this.element.style.transform = `scale(${scaleX * 0.5}, ${scaleY * 0.5})`; this.element.style.opacity = '0'; } } /** * Animate rotate effect * @param {string} direction - Animation direction ('in', 'out') * @private */ _animateRotate(direction) { const transform = this.config.motion.transform; const rotateZ = transform.rotateZ || 0; if (direction === 'in') { this.element.style.transform = `rotate(${rotateZ}deg)`; this.element.style.opacity = '1'; } else { this.element.style.transform = `rotate(${rotateZ + 90}deg)`; this.element.style.opacity = '0'; } } /** * Apply custom animation * @param {string} direction - Animation direction ('in', 'out') * @private */ _animateCustom(direction) { // Apply transform based on direction this._applyCustomTransform(direction === 'in' ? 1 : 0); } /** * Apply custom transform based on animation progress * @param {number} progress - Animation progress (0-1) * @private */ _applyCustomTransform(progress) { const t = this.config.motion.transform; // Interpolate transform values const tx = (t.translateX || 0) * progress; const ty = (t.translateY || 0) * progress; const tz = (t.translateZ || 0) * progress; const rx = (t.rotateX || 0) * progress; const ry = (t.rotateY || 0) * progress; const rz = (t.rotateZ || 0) * progress; const sx = 1 + ((t.scaleX || 1) - 1) * progress; const sy = 1 + ((t.scaleY || 1) - 1) * progress; const sz = 1 + ((t.scaleZ || 1) - 1) * progress; // Apply transform this.element.style.transform = ` translate3d(${tx}px, ${ty}px, ${tz}px) rotateX(${rx}deg) rotateY(${ry}deg) rotateZ(${rz}deg) scale3d(${sx}, ${sy}, ${sz}) `.trim(); // Apply opacity this.element.style.opacity = 0.5 + (progress * 0.5); } /** * Get scroll progress relative to element * @returns {number} - Scroll progress (0-1) * @private */ _getScrollProgress() { const rect = this.element.getBoundingClientRect(); const windowHeight = window.innerHeight; if (rect.bottom < 0) { // Element is above viewport return 0; } else if (rect.top > windowHeight) { // Element is below viewport return 0; } else { // Element is partially or fully in viewport const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0); return Math.min(1, visibleHeight / rect.height); } } /** * Play feedback sound if enabled * @private */ _playFeedbackSound() { if (this.config.feedback.sound && this.config.feedback.soundSrc) { const audio = new Audio(this.config.feedback.soundSrc); audio.volume = 0.5; // Moderate volume // Play sound with error handling audio.play().catch(error => { console.warn('Could not play audio feedback:', error); }); } } /** * Debounce function to limit how often a function is called * @param {Function} func - Function to debounce * @param {number} wait - Wait time in milliseconds * @returns {Function} - Debounced function * @private */ _debounce(func, wait) { let timeout; return function() { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args); }, wait); }; } /** * Throttle function to limit frequency of function calls * @param {Function} func - Function to throttle * @param {number} limit - Minimum time between calls in milliseconds * @returns {Function} - Throttled function * @private */ _throttle(func, limit) { let inThrottle; return function() { const context = this; const args = arguments; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } // PUBLIC API METHODS /** * Manually trigger animation * @param {string} direction - Animation direction ('in', 'out', 'toggle') */ animate(direction) { this._animate(direction); } /** * Change current state * @param {string} stateName - Name of state to transition to */ setState(stateName) { // Validate state exists if (!this.config.states.transitions.hasOwnProperty(stateName)) { console.warn(`State "${stateName}" is not defined`); return; } this.state.currentState = stateName; this._applyStateClass(stateName); } /** * Update configuration options * @param {Object} newOptions - New configuration options */ updateConfig(newOptions) { this.config = this._mergeOptions(this.config, newOptions); // Re-initialize with new config this._updateAnimationParameters(this.config); } /** * Clean up resources and event listeners */ destroy() { // Remove all event listeners this.element.removeEventListener('mouseenter', this._animate); this.element.removeEventListener('mouseleave', this._animate); this.element.removeEventListener('click', this._animate); // Remove intersection observer if (this.state.observers.inView) { this.state.observers.inView.disconnect(); } // Remove custom cursor const cursor = document.querySelector('.custom-cursor'); if (cursor) { document.body.removeChild(cursor); } // Reset element style to original this.element.style.transform = this.originalStyles.transform; this.element.style.transition = this.originalStyles.transition; this.element.style.opacity = this.originalStyles.opacity; this.element.style.willChange = ''; // Remove classes this.element.classList.remove( 'motion-interactive', 'animated-in', 'animated-out', ...Array.from(this.element.classList).filter(cls => cls.startsWith('state-')) ); } } /** * MotionSequence - Orchestrates multiple motion elements in a sequence */ class MotionSequence { /** * Creates a new motion sequence * @param {Array} elements - Array of MotionInteractive instances or selectors * @param {Object} options - Configuration options */ constructor(elements, options = {}) { // Default configuration this.config = { sequenceDelay: 200, // Delay between sequence items staggered: true, // Whether to use staggered animation direction: 'forward', // 'forward', 'reverse', 'alternate' loop: false, // Whether to loop the sequence autoplay: false, // Whether to play automatically playTrigger: null, // CSS selector for element that triggers play resetWhenOffscreen: true, // Reset when sequence goes offscreen }; // Merge user options this.config = { ...this.config, ...options }; // Convert any string selectors to MotionInteractive instances this.elements = elements.map(element => { if (element instanceof MotionInteractive) { return element; } else { // For string selectors or DOM elements, create new instance return new MotionInteractive(element, { behavior: { type: 'sequence' } // Override behavior type }); } }); // State tracking this.state = { currentIndex: 0, direction: 1, // 1 for forward, -1 for reverse isPlaying: false, observers: {} }; // Initialize sequence this._init(); } /** * Initialize motion sequence * @private */ _init() { // Set up play trigger if configured if (this.config.playTrigger) { const trigger = document.querySelector(this.config.playTrigger); if (trigger) { trigger.addEventListener('click', () => this.play()); } } // Set up in-view detection for auto reset if (this.config.resetWhenOffscreen) { this._setupInViewDetection(); } // Autoplay if configured if (this.config.autoplay) { // Slight delay for DOM to be ready setTimeout(() => this.play(), 100); } } /** * Set up in-view detection with Intersection Observer * @private */ _setupInViewDetection() { // Skip if unsupported if (!('IntersectionObserver' in window) || this.elements.length === 0) return; // Use first element as reference for in-view detection const referenceElement = this.elements[0].element; const options = { root: null, rootMargin: '0px', threshold: 0.2 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (!entry.isIntersecting && this.state.isPlaying) { // Reset sequence when scrolled out of view this.reset(); } }); }, options); observer.observe(referenceElement); this.state.observers.inView = observer; } /** * Play the motion sequence */ play() { if (this.state.isPlaying) return; this.state.isPlaying = true; // Reset to start if needed if (this.config.direction !== 'reverse') { this.state.currentIndex = 0; this.state.direction = 1; } else { this.state.currentIndex = this.elements.length - 1; this.state.direction = -1; } this._playNextInSequence(); } /** * Play next item in sequence * @private */ _playNextInSequence() { if (!this.state.isPlaying) return; const currentElement = this.elements[this.state.currentIndex]; if (!currentElement) return; // Animate current element currentElement.animate('in'); // Set up next element in sequence const nextIndex = this._getNextIndex(); // If sequence is complete if (nextIndex === -1) { // If looping is enabled, reset and continue if (this.config.loop) { setTimeout(() => { // Handle direction for alternate mode if (this.config.direction === 'alternate') { this.state.direction *= -1; } // Reset index based on direction if (this.state.direction === 1) { this.state.currentIndex = 0; } else { this.state.currentIndex = this.elements.length - 1; } this._playNextInSequence(); }, this.config.sequenceDelay); } else { // Otherwise, end sequence this.state.isPlaying = false; } } else { // Continue to next element after delay setTimeout(() => { this.state.currentIndex = nextIndex; this._playNextInSequence(); }, this.config.sequenceDelay); } } /** * Get index of next element in sequence * @returns {number} - Index of next element or -1 if sequence complete * @private */ _getNextIndex() { let nextIndex = this.state.currentIndex + this.state.direction; // Check if sequence is complete if (nextIndex < 0 || nextIndex >= this.elements.length) { return -1; } return nextIndex; } /** * Pause the motion sequence */ pause() { this.state.isPlaying = false; } /** * Reset the sequence to initial state */ reset() { this.state.isPlaying = false; // Reset all elements to initial state this.elements.forEach(element => { element.animate('out'); }); } /** * Clean up resources */ destroy() { // Clean up intersection observer if (this.state.observers.inView) { this.state.observers.inView.disconnect(); } // Destroy all element instances this.elements.forEach(element => { element.destroy(); }); } } /** * Behavioral Pattern Library - Reusable motion patterns */ const MotionPatterns = { /** * Attention-seeking pulse animation * @param {string|Element} selector - Element selector * @param {Object} options - Additional options * @returns {MotionInteractive} - Motion interactive instance */ pulse: function(selector, options = {}) { return new MotionInteractive(selector, { animationType: 'scale', duration: 600, easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)', behavior: { type: 'auto', repeat: true, interval: 2000, pauseOnHover: true }, motion: { transform: { scaleX: 1.05, scaleY: 1.05 } }, ...options }); }, /** * Floating animation that gently moves up and down * @param {string|Element} selector - Element selector * @param {Object} options - Additional options * @returns {MotionInteractive} - Motion interactive instance */ float: function(selector, options = {}) { return new MotionInteractive(selector, { animationType: 'custom', duration: 3000, easing: 'ease-in-out', behavior: { type: 'auto', repeat: true, interval: 3000 }, motion: { transform: { translateY: -10 } }, ...options }); }, /** * Reveal animation for content as it enters viewport * @param {string|Element} selector - Element selector * @param {Object} options - Additional options * @returns {MotionInteractive} - Motion interactive instance */ reveal: function(selector, options = {}) { return new MotionInteractive(selector, { animationType: 'fade', duration: 800, easing: 'cubic-bezier(0.5, 0, 0, 1)', behavior: { type: 'inview', threshold: 0.3 }, motion: { transform: { translateY: 20 } }, ...options }); }, /** * Create a staggered entrance for a group of elements * @param {string} containerSelector - Container element selector * @param {string} itemSelector - Individual item selector * @param {Object} options - Additional options * @returns {MotionSequence} - Motion sequence instance */ stagger: function(containerSelector, itemSelector, options = {}) { const container = document.querySelector(containerSelector); if (!container) return null; const items = Array.from(container.querySelectorAll(itemSelector)); const elements = items.map(item => new MotionInteractive(item, { animationType: options.animationType || 'fade', duration: options.duration || 500, easing: options.easing || 'ease-out', behavior: { type: 'sequence' }, motion: { transform: { translateY: options.translateY || 20, opacity: 0 } } })); return new MotionSequence(elements, { sequenceDelay: options.delay || 100, staggered: true, autoplay: options.autoplay !== undefined ? options.autoplay : true, ...options }); }, /** * Button hover effect with subtle scale and shadow * @param {string|Element} selector - Element selector * @param {Object} options - Additional options * @returns {MotionInteractive} - Motion interactive instance */ buttonHover: function(selector, options = {}) { const elements = document.querySelectorAll(selector); elements.forEach(element => { // Store original box-shadow const originalShadow = window.getComputedStyle(element).boxShadow; // Create instance new MotionInteractive(element, { animationType: 'scale', duration: 200, easing: 'ease-out', behavior: { type: 'hover' }, motion: { transform: { scaleX: 1.05, scaleY: 1.05 } }, ...options, // Add custom event handlers onEnter: () => { element.style.boxShadow = '0 5px 15px rgba(0,0,0,0.2)'; }, onLeave: () => { element.style.boxShadow = originalShadow; } }); }); // Return the first instance for API access (though multiple were created) return elements.length > 0 ? new MotionInteractive(elements[0], { behavior: { type: 'hover' } }) : null; } }; // Add default styles (function() { const styleEl = document.createElement('style'); styleEl.textContent = ` /* Motion Interactive Base Styles */ .motion-interactive { transition-property: transform, opacity; will-change: transform, opacity; } /* State-based styles */ .motion-interactive.state-default { /* Default state styles */ } .motion-interactive.state-hover { /* Hover state styles */ } .motion-interactive.state-active { /* Active state styles */ } /* Animation direction classes */ .motion-interactive.animated-in { /* Styles for "in" state */ } .motion-interactive.animated-out { /* Styles for "out" state */ } `; document.head.appendChild(styleEl); })(); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = { MotionInteractive, MotionSequence, MotionPatterns }; }