/ app / ui / animation-manager.js
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  }