375 lines
12 KiB
JavaScript
375 lines
12 KiB
JavaScript
/**
|
|
* Animation Queue Module
|
|
* Handles scheduling and executing animations with proper resource management
|
|
* and synchronization with TTS
|
|
*/
|
|
import { BaseModule } from './base-module.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: 1.0,
|
|
fastForwardEnabled: false
|
|
});
|
|
|
|
this.delay = 0; // Current accumulated delay
|
|
|
|
// Bind methods using parent's bindMethods utility
|
|
this.bindMethods([
|
|
'schedule',
|
|
'fastForward',
|
|
'fastForwardSequential',
|
|
'clearAll',
|
|
'setSpeed',
|
|
'beginFastForward',
|
|
'endFastForward',
|
|
'emitAnimationComplete',
|
|
'cleanupStaleTasks'
|
|
]);
|
|
}
|
|
|
|
async initialize() {
|
|
try {
|
|
this.reportProgress(20, "Initializing Animation Queue");
|
|
|
|
// Listen for speed changes from UI
|
|
document.addEventListener('animation:speed:change', (event) => {
|
|
if (event.detail && typeof event.detail.speed === 'number') {
|
|
// Word timings are already speed-scaled before they reach
|
|
// the scheduler. Keep the value only for diagnostics/API
|
|
// compatibility; do not apply it again in schedule().
|
|
this.config.speed = event.detail.speed;
|
|
console.log(`AnimationQueue: Speed updated to ${this.config.speed}`);
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
// Delays are absolute timings calculated from the prepared sentence
|
|
// duration. TTS/app speed has already been applied at that stage.
|
|
const actualDelay = this.config.fastForwardEnabled ? 0 : Math.max(0, delay);
|
|
|
|
// Record the delay for tracking
|
|
this.delay = Math.max(this.delay, delay);
|
|
|
|
// Create a timeout object
|
|
const timeoutObject = {
|
|
func: func,
|
|
delay: actualDelay,
|
|
timeoutId: null,
|
|
executed: false,
|
|
startTime: Date.now(),
|
|
|
|
// 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 && true) {
|
|
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 && true) {
|
|
// 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fast forward all pending animations
|
|
*/
|
|
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`);
|
|
|
|
// 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 });
|
|
}
|
|
|
|
fastForwardSequential(maxDuration = 320) {
|
|
if (this.timeoutQueue.length === 0) {
|
|
console.log('AnimationQueue: No animations to fast forward sequentially');
|
|
return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
const pending = this.timeoutQueue
|
|
.filter(timeout => !timeout.executed)
|
|
.map(timeout => {
|
|
if (timeout.timeoutId !== null) {
|
|
clearTimeout(timeout.timeoutId);
|
|
timeout.timeoutId = null;
|
|
}
|
|
const remaining = Math.max(0, (timeout.startTime + timeout.delay) - now);
|
|
return { timeout, remaining };
|
|
})
|
|
.sort((left, right) => left.remaining - right.remaining);
|
|
|
|
if (pending.length === 0) {
|
|
this.timeoutQueue = [];
|
|
this.endFastForward();
|
|
this.emitAnimationComplete();
|
|
return;
|
|
}
|
|
|
|
console.log(`AnimationQueue: Fast forwarding ${pending.length} pending items sequentially`);
|
|
this.beginFastForward();
|
|
|
|
const maxRemaining = Math.max(1, ...pending.map(item => item.remaining));
|
|
const compressedDuration = Math.max(80, Number(maxDuration) || 320);
|
|
|
|
pending.forEach(({ timeout, remaining }) => {
|
|
timeout.timeoutId = setTimeout(() => {
|
|
timeout.execute();
|
|
const index = this.timeoutQueue.indexOf(timeout);
|
|
if (index !== -1) {
|
|
this.timeoutQueue.splice(index, 1);
|
|
}
|
|
if (this.timeoutQueue.length === 0) {
|
|
this.delay = 0;
|
|
this.endFastForward();
|
|
this.emitAnimationComplete();
|
|
}
|
|
}, Math.round((remaining / maxRemaining) * compressedDuration));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Begin fast forwarding mode
|
|
*/
|
|
beginFastForward() {
|
|
if (this.config.fastForwardEnabled) return;
|
|
|
|
// Update config using parent's updateConfig method
|
|
this.updateConfig({ fastForwardEnabled: true });
|
|
|
|
// 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 - Stored speed value for compatibility/diagnostics
|
|
*/
|
|
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();
|
|
|
|
// Export the module
|
|
export { AnimationQueue };
|