animation-manager.js
1 import AppConstants from '../config/constants.js'; 2 import { 3 addAnimationClasses, 4 removeSpecificClasses, 5 } from '../utils/animation/animation-class-manager.js'; 6 import { 7 applyPositionStyles, 8 runAnimationSequence, 9 clearAllTimeouts, 10 clearExistingTimeout, 11 clearAnimationElementState, 12 setHideTimeout, 13 } from '../utils/animation/index.js'; 14 import { showIconAnimation } from '../utils/animation/icon-animation.js'; 15 import { isElementVisible } from './element-visibility-manager.js'; 16 17 export class AnimationManager { 18 constructor(uiManager) { 19 this.uiManager = uiManager; 20 21 this.activeAnimationTimeouts = new Map(); 22 23 this.isStartupSequenceActive = false; 24 25 this.CssClasses = AppConstants.UI.CSS_CLASSES; 26 this.AnimationPositions = AppConstants.UI.ANIMATION_POSITIONS; 27 28 this.AllAnimationIcons = [ 29 this.uiManager.get('playPauseAnimationIcon'), 30 this.uiManager.get('seekAnimationIcon'), 31 this.uiManager.get('switchAnimationIcon'), 32 this.uiManager.get('volumeAnimationIcon'), 33 ].filter(Boolean); 34 35 this.AllStateCssClasses = Object.values(this.CssClasses).filter( 36 c => typeof c === 'string' && c.startsWith('state-'), 37 ); 38 this.AllAnimationCssClasses = Object.values(this.CssClasses).filter( 39 c => typeof c === 'string' && c.startsWith('anim-'), 40 ); 41 } 42 _hideOtherAnimationIcons(currentIconElement, relatedElementKey) { 43 for (const otherIcon of this.AllAnimationIcons) { 44 if ( 45 otherIcon && 46 otherIcon !== currentIconElement && 47 isElementVisible(otherIcon, this.CssClasses.VISIBLE) 48 ) { 49 clearExistingTimeout(this.activeAnimationTimeouts, otherIcon); 50 51 clearAnimationElementState( 52 otherIcon, 53 this.AllStateCssClasses, 54 this.AllAnimationCssClasses, 55 this.CssClasses.VISIBLE, 56 ); 57 } 58 } 59 60 const relatedElement = relatedElementKey ? this.uiManager.get(relatedElementKey) : null; 61 if (relatedElement && isElementVisible(relatedElement, this.CssClasses.VISIBLE)) { 62 relatedElement.classList.remove(this.CssClasses.VISIBLE); 63 } 64 } 65 66 _prepareIconForAnimation(iconElement, stateClass, position) { 67 clearExistingTimeout(this.activeAnimationTimeouts, iconElement); 68 69 clearAnimationElementState( 70 iconElement, 71 this.AllStateCssClasses, 72 this.AllAnimationCssClasses, 73 this.CssClasses.VISIBLE, 74 ); 75 76 iconElement.classList.add(stateClass); 77 applyPositionStyles(iconElement, position); 78 } 79 80 _executeAnimation(iconElement, animationClass, durationMs) { 81 return new Promise(resolve => { 82 requestAnimationFrame(() => { 83 if (this.isStartupSequenceActive) { 84 clearAnimationElementState( 85 iconElement, 86 this.AllStateCssClasses, 87 this.AllAnimationCssClasses, 88 this.CssClasses.VISIBLE, 89 ); 90 resolve(false); 91 return; 92 } 93 94 addAnimationClasses(iconElement, null, animationClass, this.CssClasses.VISIBLE); 95 96 const hideCallback = element => { 97 clearAnimationElementState( 98 element, 99 this.AllStateCssClasses, 100 this.AllAnimationCssClasses, 101 this.CssClasses.VISIBLE, 102 ); 103 }; 104 105 setHideTimeout(iconElement, hideCallback, durationMs, this.activeAnimationTimeouts); 106 resolve(true); 107 }); 108 }); 109 } 110 async showStartupAnimation() { 111 const element = this.uiManager.get('startupAnimationElement'); 112 if (!element) { 113 return; 114 } 115 116 try { 117 removeSpecificClasses(element, [ 118 this.CssClasses.ROLLED_IN, 119 this.CssClasses.PULSATING, 120 this.CssClasses.FADING_OUT, 121 ]); 122 element.classList.add(this.CssClasses.VISIBLE); 123 } catch (error) { 124 console.error('Failed to show startup animation:', error); 125 126 element.classList.remove( 127 this.CssClasses.ROLLED_IN, 128 this.CssClasses.PULSATING, 129 this.CssClasses.FADING_OUT, 130 ); 131 element.classList.add(this.CssClasses.VISIBLE); 132 } 133 } 134 135 async startAnimationSequence() { 136 const element = this.uiManager.get('startupAnimationElement'); 137 if (!element || !this.isStartupSequenceActive) { 138 return; 139 } 140 141 try { 142 const animationSteps = [ 143 { 144 callback: () => { 145 if ( 146 this.isStartupSequenceActive && 147 isElementVisible(element, this.CssClasses.VISIBLE) 148 ) { 149 element.classList.add(this.CssClasses.VISIBLE); 150 } 151 }, 152 delay: 0, 153 }, 154 { 155 callback: () => { 156 if ( 157 this.isStartupSequenceActive && 158 isElementVisible(element, this.CssClasses.VISIBLE) 159 ) { 160 element.classList.add(this.CssClasses.ROLLED_IN); 161 } 162 }, 163 delay: 50, 164 }, 165 { 166 callback: () => { 167 if ( 168 this.isStartupSequenceActive && 169 isElementVisible(element, this.CssClasses.VISIBLE) 170 ) { 171 element.classList.add(this.CssClasses.PULSATING); 172 } 173 }, 174 delay: AppConstants.UI.STARTUP_ANIM_ROLLIN_DURATION_MS - 50, 175 }, 176 ]; 177 178 return new Promise(resolve => { 179 runAnimationSequence(animationSteps, resolve); 180 }); 181 } catch (error) { 182 console.error('Failed to start animation sequence:', error); 183 184 return new Promise(resolve => { 185 setTimeout(() => { 186 if (this.isStartupSequenceActive && element.classList.contains(this.CssClasses.VISIBLE)) { 187 element.classList.add(this.CssClasses.ROLLED_IN); 188 } 189 190 setTimeout(() => { 191 if ( 192 this.isStartupSequenceActive && 193 element.classList.contains(this.CssClasses.VISIBLE) 194 ) { 195 element.classList.add(this.CssClasses.PULSATING); 196 } 197 resolve(); 198 }, AppConstants.UI.STARTUP_ANIM_ROLLIN_DURATION_MS - 50); 199 }, 50); 200 }); 201 } 202 } 203 204 async transitionStartupToPulsatingPlay() { 205 const element = this.uiManager.get('startupAnimationElement'); 206 207 if (!element || !this.isStartupSequenceActive) { 208 return; 209 } 210 211 if (element.classList.contains(this.CssClasses.ROLLED_IN)) { 212 element.classList.add(this.CssClasses.PULSATING); 213 return; 214 } 215 216 element.classList.add(this.CssClasses.ROLLED_IN); 217 218 try { 219 setTimeout(() => { 220 if (this.isStartupSequenceActive && isElementVisible(element, this.CssClasses.VISIBLE)) { 221 element.classList.add(this.CssClasses.PULSATING); 222 } 223 }, 100); 224 } catch (error) { 225 console.error('Failed to transition startup animation:', error); 226 227 setTimeout(() => { 228 if (this.isStartupSequenceActive && element.classList.contains(this.CssClasses.VISIBLE)) { 229 element.classList.add(this.CssClasses.PULSATING); 230 } 231 }, 100); 232 } 233 } 234 235 async hideStartupAnimation() { 236 const element = this.uiManager.get('startupAnimationElement'); 237 238 try { 239 if (!element || !isElementVisible(element, this.CssClasses.VISIBLE)) { 240 this.isStartupSequenceActive = false; 241 return; 242 } 243 244 const cleanupElement = () => { 245 removeSpecificClasses(element, [ 246 this.CssClasses.VISIBLE, 247 this.CssClasses.ROLLED_IN, 248 this.CssClasses.PULSATING, 249 this.CssClasses.FADING_OUT, 250 ]); 251 252 clearAnimationElementState( 253 element, 254 this.AllStateCssClasses, 255 this.AllAnimationCssClasses, 256 this.CssClasses.VISIBLE, 257 ); 258 }; 259 260 if (!element.classList.contains(this.CssClasses.ROLLED_IN)) { 261 const animationSteps = [ 262 { 263 callback: () => element.classList.add(this.CssClasses.ROLLED_IN), 264 delay: 0, 265 }, 266 { 267 callback: () => element.classList.add(this.CssClasses.PULSATING), 268 delay: AppConstants.UI.STARTUP_ANIM_ROLLIN_DURATION_MS, 269 }, 270 { 271 callback: () => element.classList.add(this.CssClasses.FADING_OUT), 272 delay: AppConstants.UI.STARTUP_ANIM_ROLLIN_DURATION_MS, 273 }, 274 { 275 callback: cleanupElement, 276 delay: AppConstants.UI.STARTUP_ANIM_FADE_DURATION_MS, 277 }, 278 ]; 279 280 runAnimationSequence(animationSteps); 281 } else { 282 element.classList.add(this.CssClasses.FADING_OUT); 283 284 setTimeout(cleanupElement, AppConstants.UI.STARTUP_ANIM_FADE_DURATION_MS); 285 } 286 } catch (error) { 287 console.error('Failed to hide startup animation:', error); 288 289 if (element) { 290 element.classList.remove( 291 this.CssClasses.VISIBLE, 292 this.CssClasses.ROLLED_IN, 293 this.CssClasses.PULSATING, 294 this.CssClasses.FADING_OUT, 295 ); 296 } 297 this.isStartupSequenceActive = false; 298 } 299 } 300 301 async showCustomAnimation(animationType, imageUrl, position) { 302 if (this.isStartupSequenceActive) { 303 return false; 304 } 305 306 const container = this.uiManager.get('container'); 307 if (!container) { 308 console.warn('Animation container is missing, skipping animation'); 309 return false; 310 } 311 312 try { 313 await showIconAnimation({ 314 container, 315 durationMs: AppConstants.UI.ANIMATION_DURATION_MS, 316 getIcon: url => Promise.resolve(url), 317 imageUrl, 318 position, 319 isStartupActive: () => this.isStartupSequenceActive, 320 preventDuringStartup: true, 321 animationType, 322 }); 323 324 return true; 325 } catch (e) { 326 console.warn(`Error showing ${animationType} animation:`, e); 327 return false; 328 } 329 } 330 331 async cleanup() { 332 try { 333 clearAllTimeouts(this.activeAnimationTimeouts); 334 335 for (const icon of this.AllAnimationIcons) { 336 clearAnimationElementState( 337 icon, 338 this.AllStateCssClasses, 339 this.AllAnimationCssClasses, 340 this.CssClasses.VISIBLE, 341 ); 342 } 343 344 const startupElement = this.uiManager.get('startupAnimationElement'); 345 if (startupElement) { 346 removeSpecificClasses(startupElement, [ 347 this.CssClasses.VISIBLE, 348 this.CssClasses.ROLLED_IN, 349 this.CssClasses.PULSATING, 350 this.CssClasses.FADING_OUT, 351 ]); 352 353 clearAnimationElementState( 354 startupElement, 355 this.AllStateCssClasses, 356 this.AllAnimationCssClasses, 357 this.CssClasses.VISIBLE, 358 ); 359 } 360 } catch (error) { 361 console.error('Failed to clean up animations:', error); 362 363 this.activeAnimationTimeouts.forEach(clearTimeout); 364 this.activeAnimationTimeouts.clear(); 365 366 for (const icon of this.AllAnimationIcons) { 367 if (icon) { 368 icon.classList.remove(this.CssClasses.VISIBLE); 369 icon.style.transform = ''; 370 icon.style.opacity = ''; 371 } 372 } 373 374 const startupElement = this.uiManager.get('startupAnimationElement'); 375 if (startupElement) { 376 startupElement.classList.remove( 377 this.CssClasses.VISIBLE, 378 this.CssClasses.ROLLED_IN, 379 this.CssClasses.PULSATING, 380 this.CssClasses.FADING_OUT, 381 ); 382 } 383 } 384 385 this.isStartupSequenceActive = false; 386 } 387 }