import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; import { ModuleEvent } from './base-module.js'; class UIEffects extends BaseModule { constructor() { super('ui-effects'); // No external dependencies this.dependencies = []; // Effects state this.activeEffects = new Map(); this.ambientEffectsActive = false; // Effects configuration this.effectsConfig = { candleFlicker: { intensity: 0.5, speed: 0.8 }, textShadow: { enabled: true, color: 'rgba(0, 0, 0, 0.5)' }, backgroundEffects: { enabled: true } }; // Bind methods that use 'this' internally or are used as callbacks/event handlers this.initialize = this.initialize.bind(this); // Bind initialize as it calls dispatchEvent this.updateCandleEffect = this.updateCandleEffect.bind(this); // Used with requestAnimationFrame this.setupEffectElements = this.setupEffectElements.bind(this); this.createEffectsOverlay = this.createEffectsOverlay.bind(this); this.createCandleEffect = this.createCandleEffect.bind(this); this.createLightingElement = this.createLightingElement.bind(this); this.setupAmbientEffects = this.setupAmbientEffects.bind(this); this.setupCandleFlickerEffect = this.setupCandleFlickerEffect.bind(this); this.startAmbientEffects = this.startAmbientEffects.bind(this); this.stopAmbientEffects = this.stopAmbientEffects.bind(this); this.applyEffect = this.applyEffect.bind(this); this.applyShakeEffect = this.applyShakeEffect.bind(this); this.applyFlashEffect = this.applyFlashEffect.bind(this); this.applyTextEmphasis = this.applyTextEmphasis.bind(this); this.processCommand = this.processCommand.bind(this); // Store a bound version of dispatchEvent for use in methods this._dispatchModuleEvent = (name, detail) => { document.dispatchEvent(new CustomEvent(name, { detail: { moduleId: this.id, ...detail }, bubbles: true })); }; 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(); this.reportProgress(100, 'UI Effects ready'); // Use the DOM event API directly instead of this.dispatchEvent this._dispatchModuleEvent('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.effectsConfig.candleFlicker.enabled !== false) { this.setupCandleFlickerEffect(); } } setupCandleFlickerEffect() { // Store the animation frame ID for later cancellation this.candleAnimationId = null; } updateCandleEffect() { if (!this.candleEffectElement || !this.ambientEffectsActive) return; const { intensity, speed } = this.effectsConfig.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 if (this.lightingElement) { this.lightingElement.style.animation = 'gradient-animation-shrink 2s infinite alternate'; } } 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;