UNPKG

@erosnicolau/animated-text

Version:

Advanced React animated text component with comprehensive animation effects

272 lines (271 loc) 10.8 kB
/** * FPS-Based Performance Monitor - Real-time frame rate monitoring */ class FPSPerformanceMonitor { constructor() { this.frames = []; this.lastTime = performance.now(); this.animationId = null; this.activeAnimations = 0; this.isMonitoring = false; this.lowFPSStartTime = 0; this.performanceModeTimeout = null; this.manuallyForcedFlag = false; // Track if manually forced to prevent auto-recovery this.config = { fpsThreshold: 60, // 60 FPS threshold for production sampleSize: 60, activationDelay: 2000, recoveryTime: 10000, enableLogging: true, storageKey: 'fps-performance-mode' }; this.measureFrame = () => { const currentTime = performance.now(); const deltaTime = currentTime - this.lastTime; this.lastTime = currentTime; // Calculate current frame rate const frameFPS = 1000 / deltaTime; // Store in rolling window this.frames.push(frameFPS); if (this.frames.length > this.config.sampleSize) { this.frames.shift(); } // Calculate average FPS const averageFPS = this.frames.reduce((a, b) => a + b, 0) / this.frames.length; // Check for performance issues this.checkPerformance(averageFPS); // Continue monitoring if still active if (this.isMonitoring && this.activeAnimations > 0) { this.animationId = requestAnimationFrame(this.measureFrame); } else { this.stopMonitoring(); } }; this.initializeFromStorage(); } static getInstance() { if (!FPSPerformanceMonitor.instance) { FPSPerformanceMonitor.instance = new FPSPerformanceMonitor(); } return FPSPerformanceMonitor.instance; } registerAnimation(animationId) { this.activeAnimations++; if (this.config.enableLogging) { console.log(`[FPS Monitor] Animation registered: ${animationId} (${this.activeAnimations} total active)`); } if (!this.isMonitoring) { this.startMonitoring(); } } unregisterAnimation(animationId) { this.activeAnimations = Math.max(0, this.activeAnimations - 1); if (this.config.enableLogging) { console.log(`[FPS Monitor] Animation unregistered: ${animationId} (${this.activeAnimations} total active)`); } if (this.activeAnimations === 0) { this.stopMonitoring(); } } startMonitoring() { if (this.isMonitoring) return; this.isMonitoring = true; this.frames = []; this.lastTime = performance.now(); this.lowFPSStartTime = 0; if (this.config.enableLogging) { console.log('[FPS Monitor] Started monitoring'); } this.measureFrame(); } stopMonitoring() { if (!this.isMonitoring) return; this.isMonitoring = false; if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; } this.frames = []; this.lowFPSStartTime = 0; if (this.config.enableLogging) { console.log('[FPS Monitor] Stopped monitoring'); } } checkPerformance(currentFPS) { // Don't auto-trigger if manually forced if (this.manuallyForcedFlag) { return; } const now = performance.now(); if (currentFPS < this.config.fpsThreshold) { if (this.lowFPSStartTime === 0) { this.lowFPSStartTime = now; } else if (now - this.lowFPSStartTime >= this.config.activationDelay) { this.activatePerformanceMode(false); // false = auto-triggered this.lowFPSStartTime = 0; } } else { this.lowFPSStartTime = 0; } } activatePerformanceMode(isManual = false) { try { localStorage.setItem(this.config.storageKey, 'true'); localStorage.setItem(`${this.config.storageKey}-timestamp`, Date.now().toString()); if (isManual) { this.manuallyForcedFlag = true; localStorage.setItem(`${this.config.storageKey}-manual`, 'true'); } // Dispatch storage event to notify all components window.dispatchEvent(new StorageEvent('storage', { key: this.config.storageKey, newValue: 'true', storageArea: localStorage })); if (this.config.enableLogging) { console.warn(`[FPS Monitor] Performance mode ACTIVATED ${isManual ? '(manually forced)' : `(FPS below ${this.config.fpsThreshold})`}`); } // Clear any existing recovery timer if (this.performanceModeTimeout) { clearTimeout(this.performanceModeTimeout); } // Only set recovery timer if not manually forced if (!this.manuallyForcedFlag) { this.performanceModeTimeout = setTimeout(() => { this.deactivatePerformanceMode(); }, this.config.recoveryTime); } } catch { if (this.config.enableLogging) { console.warn('[FPS Monitor] localStorage unavailable'); } } } deactivatePerformanceMode() { try { localStorage.removeItem(this.config.storageKey); localStorage.removeItem(`${this.config.storageKey}-timestamp`); localStorage.removeItem(`${this.config.storageKey}-manual`); this.manuallyForcedFlag = false; // Dispatch storage event to notify all components window.dispatchEvent(new StorageEvent('storage', { key: this.config.storageKey, newValue: null, storageArea: localStorage })); if (this.config.enableLogging) { console.log('[FPS Monitor] Performance mode DEACTIVATED - system recovered'); } if (this.performanceModeTimeout) { clearTimeout(this.performanceModeTimeout); this.performanceModeTimeout = null; } } catch { if (this.config.enableLogging) { console.warn('[FPS Monitor] localStorage unavailable during deactivation'); } } } isPerformanceModeActive() { try { return localStorage.getItem(this.config.storageKey) === 'true'; } catch { return false; } } isManuallyForced() { try { return localStorage.getItem(`${this.config.storageKey}-manual`) === 'true'; } catch { return false; } } getMetrics() { const currentFPS = this.frames.length > 0 ? this.frames.reduce((a, b) => a + b, 0) / this.frames.length : 0; const timeInLowFPS = this.lowFPSStartTime > 0 ? performance.now() - this.lowFPSStartTime : 0; return { currentFPS: Math.round(currentFPS * 10) / 10, isMonitoring: this.isMonitoring, activeAnimations: this.activeAnimations, isPerformanceModeActive: this.isPerformanceModeActive(), isManuallyForced: this.isManuallyForced(), fpsThreshold: this.config.fpsThreshold, timeInLowFPS }; } updateFPSThreshold(newThreshold) { this.config.fpsThreshold = newThreshold; // No limits for debugging if (this.config.enableLogging) { console.log(`[FPS Monitor] FPS threshold updated to ${this.config.fpsThreshold}`); } } initializeFromStorage() { try { const isActive = this.isPerformanceModeActive(); const isManual = this.isManuallyForced(); if (isActive) { this.manuallyForcedFlag = isManual; if (isManual) { // If manually forced, don't set any recovery timer if (this.config.enableLogging) { console.log('[FPS Monitor] Resumed manually forced performance mode'); } } else { // If auto-triggered, continue with recovery timer const timestamp = localStorage.getItem(`${this.config.storageKey}-timestamp`); if (timestamp) { const activatedTime = parseInt(timestamp, 10); const timeElapsed = Date.now() - activatedTime; const remainingTime = Math.max(0, this.config.recoveryTime - timeElapsed); if (remainingTime > 0) { this.performanceModeTimeout = setTimeout(() => { this.deactivatePerformanceMode(); }, remainingTime); if (this.config.enableLogging) { console.log(`[FPS Monitor] Resumed auto-triggered performance mode - ${Math.round(remainingTime / 1000)}s remaining`); } } else { this.deactivatePerformanceMode(); } } } } } catch { if (this.config.enableLogging) { console.warn('[FPS Monitor] Could not initialize from localStorage'); } } } forceActivatePerformanceMode() { this.activatePerformanceMode(true); // true = manually forced } forceDeactivatePerformanceMode() { this.deactivatePerformanceMode(); } reset() { this.stopMonitoring(); this.activeAnimations = 0; this.manuallyForcedFlag = false; this.deactivatePerformanceMode(); } } // Export singleton instance export const fpsPerformanceMonitor = FPSPerformanceMonitor.getInstance(); // Export convenient static methods export const registerAnimation = (animationId) => fpsPerformanceMonitor.registerAnimation(animationId); export const unregisterAnimation = (animationId) => fpsPerformanceMonitor.unregisterAnimation(animationId); export const isPerformanceModeActive = () => fpsPerformanceMonitor.isPerformanceModeActive(); export const getFPSMetrics = () => fpsPerformanceMonitor.getMetrics(); export const updateFPSThreshold = (threshold) => fpsPerformanceMonitor.updateFPSThreshold(threshold);