import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class UIEffects extends BaseModule { constructor() { super('ui-effects', 'UI Effects'); // No external dependencies this.dependencies = []; // Effects state this.activeEffects = new Map(); this.ambientEffectsActive = false; // Effects configuration - use the config object from BaseModule this.updateConfig({ candleFlicker: { intensity: 0.5, speed: 0.8 }, textShadow: { enabled: true, color: 'rgba(0, 0, 0, 0.5)' }, backgroundEffects: { enabled: true } }); // Use bindMethods from parent class this.bindMethods([ 'updateCandleEffect', 'setupEffectElements', 'createEffectsOverlay', 'createCandleEffect', 'createLightingElement', 'setupAmbientEffects', 'setupCandleFlickerEffect', 'startAmbientEffects', 'stopAmbientEffects', 'applyEffect', 'applyShakeEffect', 'applyFlashEffect', 'applyTextEmphasis', 'processCommand', 'handleLightingAnimationEnd' ]); console.log('UIEffects: Constructor initialized'); } async initialize() { this.reportProgress(0, 'Initializing UI Effects'); try { this.reportProgress(30, 'Setting up effect elements'); // Create or get effect elements this.setupEffectElements(); this.reportProgress(60, 'Preparing ambient effects'); // Set up ambient effect animations this.setupAmbientEffects(); // Start ambient effects immediately after initialization this.startAmbientEffects(); this.reportProgress(100, 'UI Effects ready'); // Use the parent's dispatchEvent method this.dispatchEvent('ui:effects:ready', {}); return true; } catch (error) { console.error('Error initializing UI Effects:', error); this.changeState('ERROR'); return false; } } setupEffectElements() { console.log('UIEffects: Setting up effect elements'); // Create overlay for effects if it doesn't exist this.effectsOverlay = document.getElementById('effects-overlay') || this.createEffectsOverlay(); // Create candle effect element this.candleEffectElement = document.getElementById('candle-effect') || this.createCandleEffect(); // Ensure lighting element exists this.lightingElement = document.getElementById('lighting') || this.createLightingElement(); } createEffectsOverlay() { const overlay = document.createElement('div'); overlay.id = 'effects-overlay'; overlay.className = 'effects-overlay'; document.body.appendChild(overlay); return overlay; } createCandleEffect() { const candleEffect = document.createElement('div'); candleEffect.id = 'candle-effect'; candleEffect.className = 'candle-effect'; if (this.effectsOverlay) { this.effectsOverlay.appendChild(candleEffect); } else { document.body.appendChild(candleEffect); } return candleEffect; } createLightingElement() { const lighting = document.createElement('div'); lighting.id = 'lighting'; document.body.appendChild(lighting); return lighting; } setupAmbientEffects() { // Initialize candle flicker effect if (this.candleEffectElement && this.config.candleFlicker.enabled !== false) { this.setupCandleFlickerEffect(); } } setupCandleFlickerEffect() { // Store the animation frame ID for later cancellation this.candleAnimationId = null; // Add animation end event listener to create continuous random animations if (this.lightingElement) { this.lightingElement.addEventListener('animationend', this.handleLightingAnimationEnd.bind(this)); } } /** * Handle the end of a lighting animation by setting a new random duration * @param {AnimationEvent} event - The animation end event */ handleLightingAnimationEnd(event) { if (!this.lightingElement || !this.ambientEffectsActive) return; // Generate a random duration between 0.5 and 4 seconds const randomDuration = Math.random() * 3.5 + 0.5; // Toggle between grow and shrink animations const previousAnimation = event.animationName; if (previousAnimation === 'gradient-animation-grow') { this.lightingElement.style.animation = `gradient-animation-shrink ${randomDuration}s 1`; } else { this.lightingElement.style.animation = `gradient-animation-grow ${randomDuration}s 1`; } } updateCandleEffect() { if (!this.candleEffectElement || !this.ambientEffectsActive) return; const { intensity, speed } = this.config.candleFlicker; // Create subtle random flickering effect const flickerAmount = Math.random() * intensity; const opacity = 0.2 + flickerAmount * 0.2; this.candleEffectElement.style.opacity = opacity; // Schedule next update this.candleAnimationId = requestAnimationFrame(this.updateCandleEffect); } // Public methods startAmbientEffects() { if (this.ambientEffectsActive) return; this.ambientEffectsActive = true; // Start candle flicker if (this.candleEffectElement) { this.updateCandleEffect(); } // Apply lighting animation with initial random duration if (this.lightingElement) { const initialDuration = Math.random() * 3.5 + 0.5; this.lightingElement.style.animation = `gradient-animation-shrink ${initialDuration}s 1`; } } stopAmbientEffects() { this.ambientEffectsActive = false; // Stop candle flicker if (this.candleAnimationId) { cancelAnimationFrame(this.candleAnimationId); this.candleAnimationId = null; } // Stop lighting animation if (this.lightingElement) { this.lightingElement.style.animation = ''; } } applyEffect(effectName, options = {}) { switch (effectName) { case 'shake': return this.applyShakeEffect(options); case 'flash': return this.applyFlashEffect(options); case 'emphasis': return this.applyTextEmphasis(options.text, options); default: console.warn(`Unknown effect: ${effectName}`); return null; } } applyShakeEffect(options = {}) { const target = options.target || document.getElementById('book'); if (!target) return null; const intensity = options.intensity || 'medium'; const duration = options.duration || 500; // Store original styles const originalTransition = target.style.transition; const originalTransform = target.style.transform; // Apply the shake animation target.style.transition = `transform ${duration}ms ease`; target.style.transform = 'translate(0, 0)'; target.style.animation = `shake ${duration}ms 1`; // Return to normal after animation const effectId = setTimeout(() => { target.style.transition = originalTransition; target.style.transform = originalTransform; target.style.animation = ''; this.activeEffects.delete(effectId); }, duration); this.activeEffects.set(effectId, { name: 'shake', target }); return effectId; } applyFlashEffect(options = {}) { const color = options.color || 'white'; const duration = options.duration || 300; // Create flash overlay if not exists let flashOverlay = document.getElementById('flash-overlay'); if (!flashOverlay) { flashOverlay = document.createElement('div'); flashOverlay.id = 'flash-overlay'; flashOverlay.style.position = 'fixed'; flashOverlay.style.top = '0'; flashOverlay.style.left = '0'; flashOverlay.style.width = '100%'; flashOverlay.style.height = '100%'; flashOverlay.style.pointerEvents = 'none'; flashOverlay.style.zIndex = '9999'; flashOverlay.style.opacity = '0'; flashOverlay.style.transition = `opacity ${duration / 2}ms ease-in-out`; document.body.appendChild(flashOverlay); } // Set color and make visible flashOverlay.style.backgroundColor = color; // Start animation setTimeout(() => { flashOverlay.style.opacity = '0.8'; }, 10); const effectId = setTimeout(() => { flashOverlay.style.opacity = '0'; this.activeEffects.delete(effectId); // Remove element after fade out setTimeout(() => { if (flashOverlay.parentNode) { flashOverlay.parentNode.removeChild(flashOverlay); } }, duration / 2); }, duration / 2); this.activeEffects.set(effectId, { name: 'flash' }); return effectId; } applyTextEmphasis(text, options = {}) { // Use existing display handler to show emphasized text const displayHandler = moduleRegistry.getModule('ui-display-handler'); if (!displayHandler) return null; const style = { fontWeight: 'bold', color: options.color || '#990000', fontSize: options.size || '1.2em' }; return displayHandler.displayText(text, { style, speak: true }); } processCommand(command) { switch (command.action) { case 'apply': return this.applyEffect(command.effect, command.options); case 'cancel': const effectId = command.effectId; if (this.activeEffects.has(effectId)) { clearTimeout(effectId); this.activeEffects.delete(effectId); } break; case 'ambient': if (command.state === 'start') { this.startAmbientEffects(); } else if (command.state === 'stop') { this.stopAmbientEffects(); } break; default: console.warn(`Unknown effect command: ${command.action}`); } } } // Create the singleton instance const uiEffects = new UIEffects(); // Register with the module registry moduleRegistry.register(uiEffects); // Export the module export { uiEffects as UIEffects }; // Keep a reference in window for loader system console.log('UIEffects: Registering with window'); window.UIEffects = uiEffects;