/** * Animation Queue Module * Handles scheduling and executing animations with proper resource management * and synchronization with TTS */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class AnimationQueueModule extends BaseModule { constructor() { super('animation-queue', 'Animation Queue'); // Module dependencies this.dependencies = []; // Queue of scheduled animations/functions this.timeoutQueue = []; // Animation timing properties - use parent's config system this.updateConfig({ speed: 0.05, // Base animation speed (seconds per character) fastForwardEnabled: false }); this.delay = 0; // Current accumulated delay this.tts = null; // TTS module reference // Bind methods using parent's bindMethods utility this.bindMethods([ 'schedule', 'fastForward', 'clearAll', 'setSpeed', 'beginFastForward', 'endFastForward', 'emitAnimationComplete', 'cleanupStaleTasks', 'isAnyTtsSpeaking' ]); } async initialize() { try { this.reportProgress(20, "Initializing Animation Queue"); // We'll try to get the TTS module, but it's not a hard dependency // We'll check for it again at runtime when needed setTimeout(() => { // Try to get TTS module after a delay to allow it to initialize this.tts = this.getModule('tts-player'); if (!this.tts) { console.log("Animation Queue: TTS Player module not found yet, will try again when needed"); } }, 500); this.reportProgress(100, "Animation Queue ready"); return true; } catch (error) { console.error("Error initializing Animation Queue:", error); return false; } } /** * Schedule a function to execute after a delay, with optional TTS synchronization * @param {Function} func - Function to execute * @param {number} delay - Delay in milliseconds * @param {Object} options - Optional parameters including TTS text * @returns {number} - Timeout ID for cancellation */ schedule(func, delay, options = {}) { if (typeof func !== 'function') { console.error('AnimationQueue: Invalid function provided to schedule'); return -1; } // Adjust delay based on fast-forward or speed settings const actualDelay = this.config.fastForwardEnabled ? 0 : Math.max(0, delay * this.config.speed); // Record the delay for tracking this.delay = Math.max(this.delay, delay); // If we don't have a reference to the TTS module yet, try to get it if (!this.tts) { this.tts = this.getModule('tts-player'); } // Handle TTS if text is provided and TTS is available and enabled let ttsSpeaking = false; if (options.text && this.tts && typeof this.tts.isEnabled === 'function' && this.tts.isEnabled()) { // If we're fast forwarding, don't speak if (!this.config.fastForwardEnabled) { ttsSpeaking = true; // Request TTS to speak the text this.tts.speak(options.text, (result) => { ttsSpeaking = false; // Check if this was keeping the queue busy if (this.timeoutQueue.length === 0) { this.emitAnimationComplete(); } }); } } // Create a timeout object const timeoutObject = { func: func, delay: actualDelay, timeoutId: null, executed: false, startTime: Date.now(), ttsSpeaking: ttsSpeaking, // Add an execute method that marks the timeout as executed execute: function() { if (!this.executed) { this.func(); this.executed = true; } } }; // Add to queue this.timeoutQueue.push(timeoutObject); // If we're fast forwarding, execute immediately if (this.config.fastForwardEnabled) { timeoutObject.execute(); return -1; // No timeout ID since it executed immediately } // Schedule the timeout timeoutObject.timeoutId = setTimeout(() => { // Execute the function timeoutObject.execute(); // Remove from queue const index = this.timeoutQueue.indexOf(timeoutObject); if (index !== -1) { this.timeoutQueue.splice(index, 1); } // If queue is empty and no TTS is speaking, emit animation complete if (this.timeoutQueue.length === 0 && !this.isAnyTtsSpeaking()) { this.emitAnimationComplete(); } }, actualDelay); return timeoutObject.timeoutId; } /** * Emit an animation complete event */ emitAnimationComplete() { // Only emit if queue is empty and no TTS is speaking if (this.timeoutQueue.length === 0 && !this.isAnyTtsSpeaking()) { // Use parent's dispatchEvent method this.dispatchEvent('ui:animation:complete', { timestamp: Date.now() }); } } /** * Clean up any animation tasks that might have been missed * (e.g. due to browser tab being inactive) */ cleanupStaleTasks() { const now = Date.now(); const staleTasks = []; // Find stale tasks this.timeoutQueue.forEach(task => { // If task has been running for more than 10 seconds, consider it stale if (now - task.startTime > 10000 && !task.executed) { staleTasks.push(task); } }); // Execute and remove stale tasks staleTasks.forEach(task => { console.log('AnimationQueue: Cleaning up stale task'); // Clear the timeout if (task.timeoutId !== null) { clearTimeout(task.timeoutId); } // Execute the task task.execute(); // Remove from queue const index = this.timeoutQueue.indexOf(task); if (index !== -1) { this.timeoutQueue.splice(index, 1); } }); } /** * Check if any TTS is currently speaking * @returns {boolean} - True if TTS is speaking */ isAnyTtsSpeaking() { // If we don't have a reference to the TTS module yet, try to get it if (!this.tts) { this.tts = this.getModule('tts-player'); } // Check if TTS is speaking if (this.tts && typeof this.tts.isSpeaking === 'function') { return this.tts.isSpeaking(); } // Default to false if TTS module is not available return false; } /** * Fast forward all pending animations and stop TTS */ fastForward() { if (this.timeoutQueue.length === 0) { console.log('AnimationQueue: No animations to fast forward'); return; } console.log(`AnimationQueue: Fast forwarding ${this.timeoutQueue.length} pending items`); // If we don't have a reference to the TTS module yet, try to get it if (!this.tts) { this.tts = this.getModule('tts-player'); } // Stop any active TTS if (this.tts && typeof this.tts.stop === 'function') { this.tts.stop(); } // Execute all pending animations immediately this.timeoutQueue.forEach(timeout => { // Clear the timeout if (timeout.timeoutId !== null) { clearTimeout(timeout.timeoutId); timeout.timeoutId = null; } // Clear TTS flag timeout.ttsSpeaking = false; // Execute the function immediately timeout.execute(); }); // Clear the queue this.timeoutQueue = []; // Reset delay this.delay = 0; // Update config using parent's updateConfig method this.updateConfig({ fastForwardEnabled: false }); // Emit animation complete event this.emitAnimationComplete(); // Log the fastforward completion console.log('AnimationQueue: Fast forward complete'); // Use parent's dispatchEvent method this.dispatchEvent('ui:animation:fastforward', { state: false }); } /** * Begin fast forwarding mode */ beginFastForward() { if (this.config.fastForwardEnabled) return; // Update config using parent's updateConfig method this.updateConfig({ fastForwardEnabled: true }); // If we don't have a reference to the TTS module yet, try to get it if (!this.tts) { this.tts = this.getModule('tts-player'); } // Stop any active TTS if (this.tts && typeof this.tts.stop === 'function') { this.tts.stop(); } // Use parent's dispatchEvent method this.dispatchEvent('ui:animation:fastforward', { state: true }); console.log('AnimationQueue: Fast forward mode activated'); } /** * End fast forwarding mode */ endFastForward() { if (!this.config.fastForwardEnabled) return; // Update config using parent's updateConfig method this.updateConfig({ fastForwardEnabled: false }); // Use parent's dispatchEvent method this.dispatchEvent('ui:animation:fastforward', { state: false }); console.log('AnimationQueue: Fast forward mode deactivated'); } /** * Clear all scheduled animations without executing them */ clearAll() { console.log(`Animation Queue: Clearing ${this.timeoutQueue.length} pending items`); // Clear all timeouts this.timeoutQueue.forEach(timeoutObject => { if (timeoutObject.timeoutId !== null) { clearTimeout(timeoutObject.timeoutId); } }); // Clear queue this.timeoutQueue = []; // Reset delay this.delay = 0; } /** * Set the animation speed * @param {number} speed - Animation speed factor (lower is faster) */ setSpeed(speed) { if (typeof speed !== 'number' || speed <= 0) { console.error('Animation Queue: Invalid speed value'); return; } // Update config using parent's updateConfig method this.updateConfig({ speed }); console.log(`Animation Queue: Speed set to ${speed}`); } /** * Get the current animation speed * @returns {number} - Current animation speed factor */ getSpeed() { return this.config.speed; } /** * Check if fast forwarding is active * @returns {boolean} - Whether fast forwarding is active */ isFastForwarding() { return this.config.fastForwardEnabled; } /** * Get current queue length * @returns {number} - Number of items in the queue */ getQueueLength() { return this.timeoutQueue.length; } /** * Get current accumulated delay * @returns {number} - Current delay in milliseconds */ getCurrentDelay() { return this.delay; } } // Create the singleton instance const AnimationQueue = new AnimationQueueModule(); // Register with the module registry moduleRegistry.register(AnimationQueue); // Export the module export { AnimationQueue };