Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
555
resources/js/modules/api-manager/AnimationManager.js
Normal file
555
resources/js/modules/api-manager/AnimationManager.js
Normal file
@@ -0,0 +1,555 @@
|
||||
// modules/api-manager/AnimationManager.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Web Animations API Manager - High-performance animations with timeline control
|
||||
*/
|
||||
export class AnimationManager {
|
||||
constructor(config = {}) {
|
||||
this.config = config;
|
||||
this.activeAnimations = new Map();
|
||||
this.animationGroups = new Map();
|
||||
this.defaultEasing = config.easing || 'ease-out';
|
||||
this.defaultDuration = config.duration || 300;
|
||||
|
||||
// Check for Web Animations API support
|
||||
this.supported = 'animate' in Element.prototype;
|
||||
|
||||
if (!this.supported) {
|
||||
Logger.warn('[AnimationManager] Web Animations API not supported, using fallbacks');
|
||||
} else {
|
||||
Logger.info('[AnimationManager] Initialized with Web Animations API support');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate element using Web Animations API
|
||||
*/
|
||||
animate(element, keyframes, options = {}) {
|
||||
if (!element || !keyframes) {
|
||||
Logger.warn('[AnimationManager] Missing element or keyframes');
|
||||
return null;
|
||||
}
|
||||
|
||||
const animationOptions = {
|
||||
duration: this.defaultDuration,
|
||||
easing: this.defaultEasing,
|
||||
fill: 'forwards',
|
||||
...options
|
||||
};
|
||||
|
||||
const animationId = this.generateId();
|
||||
|
||||
if (this.supported) {
|
||||
const animation = element.animate(keyframes, animationOptions);
|
||||
|
||||
// Enhanced animation object
|
||||
const enhancedAnimation = this.enhanceAnimation(animation, animationId, {
|
||||
element,
|
||||
keyframes,
|
||||
options: animationOptions
|
||||
});
|
||||
|
||||
this.activeAnimations.set(animationId, enhancedAnimation);
|
||||
|
||||
// Auto-cleanup on finish
|
||||
animation.addEventListener('finish', () => {
|
||||
this.activeAnimations.delete(animationId);
|
||||
});
|
||||
|
||||
Logger.info(`[AnimationManager] Animation started: ${animationId}`);
|
||||
return enhancedAnimation;
|
||||
} else {
|
||||
return this.fallbackAnimate(element, keyframes, animationOptions, animationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create keyframe animations
|
||||
*/
|
||||
keyframes(keyframeSet) {
|
||||
// Convert CSS-like keyframes to Web Animations format
|
||||
if (Array.isArray(keyframeSet)) {
|
||||
return keyframeSet;
|
||||
}
|
||||
|
||||
// Handle object format: { '0%': {...}, '50%': {...}, '100%': {...} }
|
||||
if (typeof keyframeSet === 'object') {
|
||||
const frames = [];
|
||||
const sortedKeys = Object.keys(keyframeSet).sort((a, b) => {
|
||||
const aPercent = parseFloat(a.replace('%', ''));
|
||||
const bPercent = parseFloat(b.replace('%', ''));
|
||||
return aPercent - bPercent;
|
||||
});
|
||||
|
||||
sortedKeys.forEach(key => {
|
||||
const offset = parseFloat(key.replace('%', '')) / 100;
|
||||
frames.push({
|
||||
...keyframeSet[key],
|
||||
offset
|
||||
});
|
||||
});
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
return keyframeSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-defined animation effects
|
||||
*/
|
||||
effects = {
|
||||
// Entrance animations
|
||||
fadeIn: (duration = 300) => ([
|
||||
{ opacity: 0 },
|
||||
{ opacity: 1 }
|
||||
]),
|
||||
|
||||
slideInLeft: (distance = '100px', duration = 300) => ([
|
||||
{ transform: `translateX(-${distance})`, opacity: 0 },
|
||||
{ transform: 'translateX(0)', opacity: 1 }
|
||||
]),
|
||||
|
||||
slideInRight: (distance = '100px', duration = 300) => ([
|
||||
{ transform: `translateX(${distance})`, opacity: 0 },
|
||||
{ transform: 'translateX(0)', opacity: 1 }
|
||||
]),
|
||||
|
||||
slideInUp: (distance = '100px', duration = 300) => ([
|
||||
{ transform: `translateY(${distance})`, opacity: 0 },
|
||||
{ transform: 'translateY(0)', opacity: 1 }
|
||||
]),
|
||||
|
||||
slideInDown: (distance = '100px', duration = 300) => ([
|
||||
{ transform: `translateY(-${distance})`, opacity: 0 },
|
||||
{ transform: 'translateY(0)', opacity: 1 }
|
||||
]),
|
||||
|
||||
scaleIn: (scale = 0.8, duration = 300) => ([
|
||||
{ transform: `scale(${scale})`, opacity: 0 },
|
||||
{ transform: 'scale(1)', opacity: 1 }
|
||||
]),
|
||||
|
||||
rotateIn: (rotation = '-180deg', duration = 600) => ([
|
||||
{ transform: `rotate(${rotation})`, opacity: 0 },
|
||||
{ transform: 'rotate(0deg)', opacity: 1 }
|
||||
]),
|
||||
|
||||
// Exit animations
|
||||
fadeOut: (duration = 300) => ([
|
||||
{ opacity: 1 },
|
||||
{ opacity: 0 }
|
||||
]),
|
||||
|
||||
slideOutLeft: (distance = '100px', duration = 300) => ([
|
||||
{ transform: 'translateX(0)', opacity: 1 },
|
||||
{ transform: `translateX(-${distance})`, opacity: 0 }
|
||||
]),
|
||||
|
||||
slideOutRight: (distance = '100px', duration = 300) => ([
|
||||
{ transform: 'translateX(0)', opacity: 1 },
|
||||
{ transform: `translateX(${distance})`, opacity: 0 }
|
||||
]),
|
||||
|
||||
scaleOut: (scale = 0.8, duration = 300) => ([
|
||||
{ transform: 'scale(1)', opacity: 1 },
|
||||
{ transform: `scale(${scale})`, opacity: 0 }
|
||||
]),
|
||||
|
||||
// Attention seekers
|
||||
bounce: (intensity = '20px', duration = 600) => ([
|
||||
{ transform: 'translateY(0)' },
|
||||
{ transform: `translateY(-${intensity})`, offset: 0.25 },
|
||||
{ transform: 'translateY(0)', offset: 0.5 },
|
||||
{ transform: `translateY(-${intensity})`, offset: 0.75 },
|
||||
{ transform: 'translateY(0)' }
|
||||
]),
|
||||
|
||||
pulse: (scale = 1.1, duration = 600) => ([
|
||||
{ transform: 'scale(1)' },
|
||||
{ transform: `scale(${scale})`, offset: 0.5 },
|
||||
{ transform: 'scale(1)' }
|
||||
]),
|
||||
|
||||
shake: (distance = '10px', duration = 600) => ([
|
||||
{ transform: 'translateX(0)' },
|
||||
{ transform: `translateX(-${distance})`, offset: 0.1 },
|
||||
{ transform: `translateX(${distance})`, offset: 0.2 },
|
||||
{ transform: `translateX(-${distance})`, offset: 0.3 },
|
||||
{ transform: `translateX(${distance})`, offset: 0.4 },
|
||||
{ transform: `translateX(-${distance})`, offset: 0.5 },
|
||||
{ transform: `translateX(${distance})`, offset: 0.6 },
|
||||
{ transform: `translateX(-${distance})`, offset: 0.7 },
|
||||
{ transform: `translateX(${distance})`, offset: 0.8 },
|
||||
{ transform: `translateX(-${distance})`, offset: 0.9 },
|
||||
{ transform: 'translateX(0)' }
|
||||
]),
|
||||
|
||||
rubberBand: (duration = 1000) => ([
|
||||
{ transform: 'scale(1)' },
|
||||
{ transform: 'scale(1.25, 0.75)', offset: 0.3 },
|
||||
{ transform: 'scale(0.75, 1.25)', offset: 0.4 },
|
||||
{ transform: 'scale(1.15, 0.85)', offset: 0.5 },
|
||||
{ transform: 'scale(0.95, 1.05)', offset: 0.65 },
|
||||
{ transform: 'scale(1.05, 0.95)', offset: 0.75 },
|
||||
{ transform: 'scale(1)' }
|
||||
]),
|
||||
|
||||
// Loading animations
|
||||
spin: (duration = 1000) => ([
|
||||
{ transform: 'rotate(0deg)' },
|
||||
{ transform: 'rotate(360deg)' }
|
||||
]),
|
||||
|
||||
heartbeat: (scale = 1.3, duration = 1000) => ([
|
||||
{ transform: 'scale(1)' },
|
||||
{ transform: `scale(${scale})`, offset: 0.14 },
|
||||
{ transform: 'scale(1)', offset: 0.28 },
|
||||
{ transform: `scale(${scale})`, offset: 0.42 },
|
||||
{ transform: 'scale(1)', offset: 0.70 }
|
||||
])
|
||||
};
|
||||
|
||||
/**
|
||||
* Predefined easing functions
|
||||
*/
|
||||
easings = {
|
||||
linear: 'linear',
|
||||
ease: 'ease',
|
||||
easeIn: 'ease-in',
|
||||
easeOut: 'ease-out',
|
||||
easeInOut: 'ease-in-out',
|
||||
|
||||
// Custom cubic-bezier easings
|
||||
easeInQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
easeOutQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
easeInOutQuad: 'cubic-bezier(0.455, 0.03, 0.515, 0.955)',
|
||||
|
||||
easeInCubic: 'cubic-bezier(0.32, 0, 0.67, 0)',
|
||||
easeOutCubic: 'cubic-bezier(0.33, 1, 0.68, 1)',
|
||||
easeInOutCubic: 'cubic-bezier(0.65, 0, 0.35, 1)',
|
||||
|
||||
easeInQuart: 'cubic-bezier(0.5, 0, 0.75, 0)',
|
||||
easeOutQuart: 'cubic-bezier(0.25, 1, 0.5, 1)',
|
||||
easeInOutQuart: 'cubic-bezier(0.76, 0, 0.24, 1)',
|
||||
|
||||
// Bouncy easings
|
||||
easeOutBack: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
easeInBack: 'cubic-bezier(0.36, 0, 0.66, -0.56)',
|
||||
easeInOutBack: 'cubic-bezier(0.68, -0.6, 0.32, 1.6)'
|
||||
};
|
||||
|
||||
/**
|
||||
* Quick effect methods
|
||||
*/
|
||||
fadeIn(element, options = {}) {
|
||||
return this.animate(element, this.effects.fadeIn(), {
|
||||
duration: 300,
|
||||
easing: this.easings.easeOut,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
fadeOut(element, options = {}) {
|
||||
return this.animate(element, this.effects.fadeOut(), {
|
||||
duration: 300,
|
||||
easing: this.easings.easeIn,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
slideIn(element, direction = 'up', options = {}) {
|
||||
const effectMap = {
|
||||
up: 'slideInUp',
|
||||
down: 'slideInDown',
|
||||
left: 'slideInLeft',
|
||||
right: 'slideInRight'
|
||||
};
|
||||
|
||||
return this.animate(element, this.effects[effectMap[direction]](), {
|
||||
duration: 400,
|
||||
easing: this.easings.easeOutBack,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
bounce(element, options = {}) {
|
||||
return this.animate(element, this.effects.bounce(), {
|
||||
duration: 600,
|
||||
easing: this.easings.easeInOut,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
pulse(element, options = {}) {
|
||||
return this.animate(element, this.effects.pulse(), {
|
||||
duration: 600,
|
||||
easing: this.easings.easeInOut,
|
||||
iterations: Infinity,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation groups - animate multiple elements together
|
||||
*/
|
||||
group(animations, options = {}) {
|
||||
const groupId = this.generateId('group');
|
||||
const animationPromises = [];
|
||||
|
||||
animations.forEach(({ element, keyframes, animationOptions = {} }) => {
|
||||
const animation = this.animate(element, keyframes, {
|
||||
...animationOptions,
|
||||
...options
|
||||
});
|
||||
|
||||
if (animation && animation.finished) {
|
||||
animationPromises.push(animation.finished);
|
||||
}
|
||||
});
|
||||
|
||||
const groupController = {
|
||||
id: groupId,
|
||||
finished: Promise.all(animationPromises),
|
||||
play: () => {
|
||||
animations.forEach(anim => {
|
||||
if (anim.animation) anim.animation.play();
|
||||
});
|
||||
},
|
||||
pause: () => {
|
||||
animations.forEach(anim => {
|
||||
if (anim.animation) anim.animation.pause();
|
||||
});
|
||||
},
|
||||
reverse: () => {
|
||||
animations.forEach(anim => {
|
||||
if (anim.animation) anim.animation.reverse();
|
||||
});
|
||||
},
|
||||
cancel: () => {
|
||||
animations.forEach(anim => {
|
||||
if (anim.animation) anim.animation.cancel();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.animationGroups.set(groupId, groupController);
|
||||
return groupController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Staggered animations - animate elements with delay
|
||||
*/
|
||||
stagger(elements, keyframes, options = {}) {
|
||||
const staggerDelay = options.staggerDelay || 100;
|
||||
const animations = [];
|
||||
|
||||
elements.forEach((element, index) => {
|
||||
const delay = index * staggerDelay;
|
||||
const animation = this.animate(element, keyframes, {
|
||||
...options,
|
||||
delay
|
||||
});
|
||||
animations.push({ element, animation });
|
||||
});
|
||||
|
||||
return this.group(animations.map(({ element, animation }) => ({
|
||||
element,
|
||||
keyframes,
|
||||
animation
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeline animations - sequence animations
|
||||
*/
|
||||
timeline(sequence) {
|
||||
const timelineId = this.generateId('timeline');
|
||||
let currentTime = 0;
|
||||
const animations = [];
|
||||
|
||||
sequence.forEach(step => {
|
||||
const delay = step.at !== undefined ? step.at : currentTime;
|
||||
const animation = this.animate(step.element, step.keyframes, {
|
||||
...step.options,
|
||||
delay
|
||||
});
|
||||
|
||||
animations.push(animation);
|
||||
|
||||
if (step.at === undefined) {
|
||||
currentTime += step.options?.duration || this.defaultDuration;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: timelineId,
|
||||
animations,
|
||||
finished: Promise.all(animations.map(a => a.finished)),
|
||||
play: () => animations.forEach(a => a.play()),
|
||||
pause: () => animations.forEach(a => a.pause()),
|
||||
reverse: () => animations.forEach(a => a.reverse()),
|
||||
cancel: () => animations.forEach(a => a.cancel())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll-triggered animations
|
||||
*/
|
||||
onScroll(element, keyframes, options = {}) {
|
||||
const scrollTrigger = options.trigger || element;
|
||||
const startOffset = options.start || 0;
|
||||
const endOffset = options.end || 1;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const progress = Math.min(Math.max(
|
||||
(entry.intersectionRatio - startOffset) / (endOffset - startOffset),
|
||||
0
|
||||
), 1);
|
||||
|
||||
if (progress >= 0) {
|
||||
this.animate(element, keyframes, {
|
||||
...options,
|
||||
duration: options.duration || this.defaultDuration
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
threshold: this.createThresholdArray(20)
|
||||
});
|
||||
|
||||
observer.observe(scrollTrigger);
|
||||
|
||||
return {
|
||||
observer,
|
||||
disconnect: () => observer.disconnect()
|
||||
};
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
enhanceAnimation(animation, id, metadata) {
|
||||
return {
|
||||
id,
|
||||
animation,
|
||||
metadata,
|
||||
|
||||
// Enhanced controls
|
||||
play: () => animation.play(),
|
||||
pause: () => animation.pause(),
|
||||
reverse: () => animation.reverse(),
|
||||
cancel: () => animation.cancel(),
|
||||
finish: () => animation.finish(),
|
||||
|
||||
// Properties
|
||||
get currentTime() { return animation.currentTime; },
|
||||
set currentTime(time) { animation.currentTime = time; },
|
||||
|
||||
get playbackRate() { return animation.playbackRate; },
|
||||
set playbackRate(rate) { animation.playbackRate = rate; },
|
||||
|
||||
get playState() { return animation.playState; },
|
||||
get finished() { return animation.finished; },
|
||||
|
||||
// Enhanced methods
|
||||
seek(progress) {
|
||||
animation.currentTime = animation.effect.getTiming().duration * progress;
|
||||
},
|
||||
|
||||
setProgress(progress) {
|
||||
this.seek(Math.max(0, Math.min(1, progress)));
|
||||
},
|
||||
|
||||
onFinish(callback) {
|
||||
animation.addEventListener('finish', callback);
|
||||
},
|
||||
|
||||
onCancel(callback) {
|
||||
animation.addEventListener('cancel', callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fallbackAnimate(element, keyframes, options, id) {
|
||||
Logger.info('[AnimationManager] Using CSS fallback animation');
|
||||
|
||||
// Simple CSS transition fallback
|
||||
const finalFrame = keyframes[keyframes.length - 1];
|
||||
element.style.transition = `all ${options.duration}ms ${options.easing}`;
|
||||
|
||||
// Apply final styles
|
||||
Object.assign(element.style, finalFrame);
|
||||
|
||||
// Return promise-like object
|
||||
return {
|
||||
id,
|
||||
finished: new Promise(resolve => setTimeout(resolve, options.duration)),
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
cancel: () => {
|
||||
element.style.transition = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generateId(prefix = 'anim') {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
createThresholdArray(steps) {
|
||||
const thresholds = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
thresholds.push(i / steps);
|
||||
}
|
||||
return thresholds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active animations
|
||||
*/
|
||||
getActiveAnimations() {
|
||||
return Array.from(this.activeAnimations.entries()).map(([id, animation]) => ({
|
||||
id,
|
||||
playState: animation.playState,
|
||||
currentTime: animation.currentTime,
|
||||
metadata: animation.metadata
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all active animations
|
||||
*/
|
||||
cancelAll() {
|
||||
this.activeAnimations.forEach(animation => {
|
||||
animation.cancel();
|
||||
});
|
||||
this.activeAnimations.clear();
|
||||
Logger.info('[AnimationManager] All animations cancelled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause all active animations
|
||||
*/
|
||||
pauseAll() {
|
||||
this.activeAnimations.forEach(animation => {
|
||||
animation.pause();
|
||||
});
|
||||
Logger.info('[AnimationManager] All animations paused');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all paused animations
|
||||
*/
|
||||
resumeAll() {
|
||||
this.activeAnimations.forEach(animation => {
|
||||
if (animation.playState === 'paused') {
|
||||
animation.play();
|
||||
}
|
||||
});
|
||||
Logger.info('[AnimationManager] All animations resumed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user