UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

955 lines (952 loc) 29.9 kB
import { ShaderResourceManager } from './ShaderResourceManager.js'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); class ResourceManager { /** * Creates a new ResourceManager instance * * @param componentId - Unique identifier for this component instance * @param options - Configuration options */ constructor(componentId, options = {}) { // Resource collections __publicField(this, "textures", /* @__PURE__ */ new Map()); __publicField(this, "filters", /* @__PURE__ */ new Set()); __publicField(this, "displayObjects", /* @__PURE__ */ new Set()); __publicField(this, "pixiApps", /* @__PURE__ */ new Set()); __publicField(this, "animations", /* @__PURE__ */ new Set()); // Event listener tracking with improved nesting __publicField(this, "listeners", /* @__PURE__ */ new Map()); // Timer tracking __publicField(this, "timeouts", /* @__PURE__ */ new Set()); __publicField(this, "intervals", /* @__PURE__ */ new Set()); // Manager state __publicField(this, "disposed", false); __publicField(this, "unmounting", false); __publicField(this, "componentId"); __publicField(this, "options"); // Performance metrics (optional) __publicField(this, "metrics", null); __publicField(this, "autoCleanupTimer", null); // Shader resource manager __publicField(this, "shaderManager", null); this.componentId = componentId; this.options = { logLevel: options.logLevel || "warn", enableMetrics: options.enableMetrics || false, autoCleanupInterval: options.autoCleanupInterval || null, enableShaderPooling: options.enableShaderPooling !== false // Enable by default }; if (this.options.enableMetrics) { this.metrics = { operations: {}, batchStats: { textures: this.createEmptyBatchStats(), filters: this.createEmptyBatchStats(), displayObjects: this.createEmptyBatchStats(), animations: this.createEmptyBatchStats() } }; } if (this.options.enableShaderPooling) { this.shaderManager = ShaderResourceManager.getInstance({ debug: this.options.logLevel === "debug", enableMetrics: this.options.enableMetrics }); this.log("info", "Shader pooling enabled"); } this.log("info", `ResourceManager initialized`); this.setupAutoCleanup(); } /** * Creates an empty batch statistics object */ createEmptyBatchStats() { return { totalBatches: 0, totalItems: 0, averageBatchSize: 0, largestBatch: 0 }; } /** * Set up automatic cleanup interval */ setupAutoCleanup() { if (this.options.autoCleanupInterval) { this.autoCleanupTimer = setInterval(() => { this.performAutoCleanup(); }, this.options.autoCleanupInterval); } } /** * Perform automatic cleanup of unused resources */ performAutoCleanup() { if (this.disposed || this.unmounting) return; this.log("debug", "Performing automatic resource cleanup..."); let texturesReleased = 0; this.textures.forEach((entry, url) => { if (entry.refCount <= 0) { this.releaseTexture(url); texturesReleased++; } }); let animationsReleased = 0; this.animations.forEach((animation) => { if (animation.isActive() === false) { animation.kill(); this.animations.delete(animation); animationsReleased++; } }); this.log("debug", `Auto cleanup complete: ${texturesReleased} textures and ${animationsReleased} animations released`); } /** * Log a message with the appropriate level * * @param level - Log level * @param message - Log message * @param data - Optional data to log */ log(level, message, data) { const logLevels = { "error": 0, "warn": 1, "info": 2, "debug": 3 }; if (logLevels[level] <= logLevels[this.options.logLevel || "warn"]) { const logMessage = `[ResourceManager:${this.componentId}] ${message}`; switch (level) { case "error": console.error(logMessage, data); break; case "warn": console.warn(logMessage, data); break; case "info": console.info(logMessage, data); break; case "debug": console.debug(logMessage, data); break; } } } /** * Records performance metric for an operation * * @param name - Operation name * @param startTime - Start time of the operation */ recordMetric(name, startTime) { if (!this.metrics) return; const endTime = performance.now(); const duration = endTime - startTime; if (!this.metrics.operations[name]) { this.metrics.operations[name] = { count: 0, totalTime: 0, averageTime: 0 }; } const op = this.metrics.operations[name]; op.count++; op.totalTime += duration; op.averageTime = op.totalTime / op.count; } /** * Update batch statistics * * @param type - Resource type * @param batchSize - Size of the batch */ updateBatchStats(type, batchSize) { if (!this.metrics) return; const stats = this.metrics.batchStats[type]; stats.totalBatches++; stats.totalItems += batchSize; stats.averageBatchSize = stats.totalItems / stats.totalBatches; stats.largestBatch = Math.max(stats.largestBatch, batchSize); } /** * Mark component as unmounting to prevent new resource allocations */ markUnmounting() { this.unmounting = true; this.log("info", "Component marked as unmounting"); } /** * Check if the resource manager is active and can allocate resources */ isActive() { return !this.unmounting && !this.disposed; } // ===== FILTER CONTROL METHODS ===== /** * Safely disable a filter to ensure it has no visible effect * * @param filter - Filter to disable */ disableFilter(filter) { try { if ("enabled" in filter && typeof filter.enabled === "boolean") { filter.enabled = false; } if ("alpha" in filter && typeof filter.alpha === "number") { filter.alpha = 0; } if ("strength" in filter && typeof filter.strength === "number") { filter.strength = 0; } if ("scale" in filter) { const scale = filter.scale; if (scale && typeof scale.x === "number" && typeof scale.y === "number") { scale.x = 0; scale.y = 0; } else if (typeof scale === "number") { filter.scale = 0; } } if ("blur" in filter && typeof filter.blur === "number") { filter.blur = 0; } } catch (error) { this.log("debug", "Error disabling filter", error); } } /** * Disable all filters on a display object * * @param displayObject - The display object whose filters should be disabled */ disableFiltersOnObject(displayObject) { if (!displayObject.filters) return; try { if (Array.isArray(displayObject.filters)) { displayObject.filters.forEach((filter) => { if (filter) this.disableFilter(filter); }); } else if (displayObject.filters) { this.disableFilter(displayObject.filters); } this.log("debug", "Disabled filters on display object"); } catch (error) { this.log("warn", "Error disabling filters on object", error); } } /** * Initialize a filter in a disabled state * This ensures the filter has no effect when first created * * @param filter - Filter to initialize * @returns The initialized filter */ initializeFilterDisabled(filter) { this.disableFilter(filter); return filter; } /** * Batch initialize and disable filters * * @param filters - Array of filters to initialize disabled * @returns The array of initialized filters */ initializeFilterBatchDisabled(filters) { filters.forEach((filter) => this.disableFilter(filter)); return filters; } /** * Track a filter * * @param filter - Filter to track * @returns The filter for chaining */ trackFilter(filter) { if (!this.isActive()) return filter; const startTime = this.metrics ? performance.now() : 0; this.filters.add(filter); if (this.shaderManager) { this.log("debug", `Filter registered with shader manager`); } if (this.metrics) { this.recordMetric("trackFilter", startTime); this.updateBatchStats("filters", 1); } return filter; } /** * Dispose of a single filter */ disposeFilter(filter) { const startTime = this.metrics ? performance.now() : 0; try { this.disableFilter(filter); if (this.shaderManager) { this.shaderManager.releaseShader(filter); } filter.destroy(); } catch (error) { this.log("debug", `Using fallback disposal for filter`, error); this.disableFilter(filter); } this.filters.delete(filter); if (this.metrics) { this.recordMetric("disposeFilter", startTime); } } /** * Get shader manager instance * * @returns The shader manager instance or null if not enabled */ getShaderManager() { return this.shaderManager; } // ===== BATCH TRACKING METHODS ===== /** * Track multiple textures at once * * @param textures - Map of URL to texture * @returns The same map for chaining */ trackTextureBatch(textures) { if (!this.isActive()) return textures; const startTime = this.metrics ? performance.now() : 0; textures.forEach((texture, url) => { const entry = this.textures.get(url); if (entry) { entry.refCount++; entry.lastUsed = Date.now(); } else { this.textures.set(url, { resource: texture, refCount: 1, lastUsed: Date.now() }); } }); if (this.metrics) { this.recordMetric("trackTextureBatch", startTime); this.updateBatchStats("textures", textures.size); } this.log("debug", `Tracked ${textures.size} textures in batch`); return textures; } /** * Track multiple filters at once * * @param filters - Array of filters to track * @returns The same array for chaining */ trackFilterBatch(filters) { if (!this.isActive()) return filters; const startTime = this.metrics ? performance.now() : 0; filters.forEach((filter) => this.filters.add(filter)); if (this.metrics) { this.recordMetric("trackFilterBatch", startTime); this.updateBatchStats("filters", filters.length); } this.log("debug", `Tracked ${filters.length} filters in batch`); return filters; } /** * Track multiple display objects at once * * @param objects - Array of display objects to track * @returns The same array for chaining */ trackDisplayObjectBatch(objects) { if (!this.isActive()) return objects; const startTime = this.metrics ? performance.now() : 0; objects.forEach((object) => this.displayObjects.add(object)); if (this.metrics) { this.recordMetric("trackDisplayObjectBatch", startTime); this.updateBatchStats("displayObjects", objects.length); } this.log("debug", `Tracked ${objects.length} display objects in batch`); return objects; } /** * Track multiple animations at once * * @param animations - Array of animations to track * @returns The same array for chaining */ trackAnimationBatch(animations) { if (!this.isActive()) return animations; const startTime = this.metrics ? performance.now() : 0; animations.forEach((animation) => this.animations.add(animation)); if (this.metrics) { this.recordMetric("trackAnimationBatch", startTime); this.updateBatchStats("animations", animations.length); } this.log("debug", `Tracked ${animations.length} animations in batch`); return animations; } /** * Track event listeners in batch * * @param element - DOM element * @param listeners - Map of event types to callbacks */ addEventListenerBatch(element, listeners) { if (!this.isActive()) return; const startTime = this.metrics ? performance.now() : 0; let count = 0; if (!this.listeners.has(element)) { this.listeners.set(element, /* @__PURE__ */ new Map()); } const elementListeners = this.listeners.get(element); listeners.forEach((callbacks, eventType) => { if (!elementListeners.has(eventType)) { elementListeners.set(eventType, /* @__PURE__ */ new Set()); } const callbackSet = elementListeners.get(eventType); callbacks.forEach((callback) => { callbackSet.add(callback); element.addEventListener(eventType, callback); count++; }); }); if (this.metrics) { this.recordMetric("addEventListenerBatch", startTime); } this.log("debug", `Added ${count} event listeners in batch`); } // ===== INDIVIDUAL TRACKING METHODS ===== /** * Track a GSAP animation * * @param animation - Animation to track * @returns The animation for chaining */ trackAnimation(animation) { if (!this.isActive()) { animation.kill(); return animation; } const startTime = this.metrics ? performance.now() : 0; this.animations.add(animation); if (this.metrics) { this.recordMetric("trackAnimation", startTime); this.updateBatchStats("animations", 1); } return animation; } /** * Track a texture * * @param url - Texture URL * @param texture - Texture to track * @returns The texture for chaining */ trackTexture(url, texture) { if (!this.isActive()) return texture; const startTime = this.metrics ? performance.now() : 0; const entry = this.textures.get(url); if (entry) { entry.refCount++; entry.lastUsed = Date.now(); } else { this.textures.set(url, { resource: texture, refCount: 1, lastUsed: Date.now() }); } if (this.metrics) { this.recordMetric("trackTexture", startTime); this.updateBatchStats("textures", 1); } return texture; } /** * Release a texture, destroying it when no longer referenced */ releaseTexture(url) { const entry = this.textures.get(url); if (!entry) return; const startTime = this.metrics ? performance.now() : 0; entry.refCount--; if (entry.refCount <= 0) { try { entry.resource.destroy(true); this.textures.delete(url); this.log("debug", `Destroyed texture: ${url}`); } catch (error) { this.log("warn", `Failed to destroy texture: ${url}`, error); } } if (this.metrics) { this.recordMetric("releaseTexture", startTime); } } /** * Track a PIXI Application * * @param app - Application to track * @returns The application for chaining */ trackPixiApp(app) { if (!this.isActive()) { this.disposePixiApp(app); return app; } const startTime = this.metrics ? performance.now() : 0; this.pixiApps.add(app); if (this.metrics) { this.recordMetric("trackPixiApp", startTime); } this.log("info", "Tracking PIXI application"); return app; } /** * Dispose of a PIXI Application */ disposePixiApp(app) { const startTime = this.metrics ? performance.now() : 0; try { app.stop(); if (app.canvas instanceof HTMLCanvasElement) { app.canvas.remove(); } app.destroy(true, { children: true }); this.log("info", "PIXI application destroyed"); } catch (error) { this.log("warn", "Error disposing PIXI application", error); } this.pixiApps.delete(app); if (this.metrics) { this.recordMetric("disposePixiApp", startTime); } } /** * Track a display object * * @param displayObject - Display object to track * @returns The display object for chaining */ trackDisplayObject(displayObject) { if (!this.isActive()) return displayObject; const startTime = this.metrics ? performance.now() : 0; this.displayObjects.add(displayObject); if (this.metrics) { this.recordMetric("trackDisplayObject", startTime); this.updateBatchStats("displayObjects", 1); } return displayObject; } /** * Dispose of a display object */ disposeDisplayObject(displayObject) { const startTime = this.metrics ? performance.now() : 0; try { if (displayObject.parent) { displayObject.parent.removeChild(displayObject); } this.disableFiltersOnObject(displayObject); if (displayObject.filters) { if (Array.isArray(displayObject.filters)) { displayObject.filters.forEach((filter) => { this.disposeFilter(filter); }); } else { this.disposeFilter(displayObject.filters); } displayObject.filters = []; } displayObject.destroy({ children: true, texture: false }); } catch (error) { this.log("warn", "Error disposing display object", error); } this.displayObjects.delete(displayObject); if (this.metrics) { this.recordMetric("disposeDisplayObject", startTime); } } /** * Add an event listener with tracking */ addEventListener(element, eventType, callback) { if (!this.isActive()) return; const startTime = this.metrics ? performance.now() : 0; if (!this.listeners.has(element)) { this.listeners.set(element, /* @__PURE__ */ new Map()); } const elementListeners = this.listeners.get(element); if (!elementListeners.has(eventType)) { elementListeners.set(eventType, /* @__PURE__ */ new Set()); } const callbacks = elementListeners.get(eventType); callbacks.add(callback); element.addEventListener(eventType, callback); if (this.metrics) { this.recordMetric("addEventListener", startTime); } } /** * Remove all event listeners */ removeAllEventListeners() { const startTime = this.metrics ? performance.now() : 0; let count = 0; this.listeners.forEach((eventMap, element) => { eventMap.forEach((callbacks, eventType) => { callbacks.forEach((callback) => { element.removeEventListener(eventType, callback); count++; }); }); }); this.listeners.clear(); if (this.metrics) { this.recordMetric("removeAllEventListeners", startTime); } this.log("debug", `Removed ${count} event listeners`); } /** * Create a setTimeout with tracking */ setTimeout(callback, delay) { if (!this.isActive()) return setTimeout(() => { }, 0); const startTime = this.metrics ? performance.now() : 0; const timeout = setTimeout(() => { this.timeouts.delete(timeout); callback(); }, delay); this.timeouts.add(timeout); if (this.metrics) { this.recordMetric("setTimeout", startTime); } return timeout; } /** * Create a setInterval with tracking */ setInterval(callback, delay) { if (!this.isActive()) return setInterval(() => { }, 0); const startTime = this.metrics ? performance.now() : 0; const interval = setInterval(callback, delay); this.intervals.add(interval); if (this.metrics) { this.recordMetric("setInterval", startTime); } return interval; } /** * Clear a tracked timeout */ clearTimeout(id) { const startTime = this.metrics ? performance.now() : 0; globalThis.clearTimeout(id); this.timeouts.delete(id); if (this.metrics) { this.recordMetric("clearTimeout", startTime); } } /** * Clear a tracked interval */ clearInterval(id) { const startTime = this.metrics ? performance.now() : 0; globalThis.clearInterval(id); this.intervals.delete(id); if (this.metrics) { this.recordMetric("clearInterval", startTime); } } /** * Clear all tracked timeouts */ clearAllTimeouts() { const startTime = this.metrics ? performance.now() : 0; let count = 0; this.timeouts.forEach((id) => { globalThis.clearTimeout(id); count++; }); this.timeouts.clear(); if (this.metrics) { this.recordMetric("clearAllTimeouts", startTime); } this.log("debug", `Cleared ${count} timeouts`); } /** * Clear all tracked intervals */ clearAllIntervals() { const startTime = this.metrics ? performance.now() : 0; let count = 0; this.intervals.forEach((id) => { globalThis.clearInterval(id); count++; }); this.intervals.clear(); if (this.metrics) { this.recordMetric("clearAllIntervals", startTime); } this.log("debug", `Cleared ${count} intervals`); } /** * Get current resource statistics * * @returns An object containing counts of various tracked resources */ getStats() { const result = { textures: this.textures.size, filters: this.filters.size, displayObjects: this.displayObjects.size, animations: this.animations.size, eventTargets: this.listeners.size, timeouts: this.timeouts.size, intervals: this.intervals.size, pixiApps: this.pixiApps.size, shaderPoolingEnabled: !!this.shaderManager }; if (this.metrics) { result.metrics = this.metrics; } if (this.shaderManager) { result.shaderManager = this.shaderManager.getStats(); } return result; } /** * Clear all tracked resources */ dispose() { if (this.disposed) return; const startTime = this.metrics ? performance.now() : 0; this.log("info", "Disposing all resources..."); this.disposed = true; this.markUnmounting(); if (this.autoCleanupTimer) { clearInterval(this.autoCleanupTimer); this.autoCleanupTimer = null; } this.animations.forEach((animation) => animation.kill()); this.animations.clear(); this.removeAllEventListeners(); this.pixiApps.forEach((app) => this.disposePixiApp(app)); this.pixiApps.clear(); this.displayObjects.forEach((obj) => this.disableFiltersOnObject(obj)); this.displayObjects.forEach((obj) => this.disposeDisplayObject(obj)); this.displayObjects.clear(); this.filters.forEach((filter) => this.disposeFilter(filter)); this.filters.clear(); this.textures.forEach((entry, url) => { try { entry.resource.destroy(true); } catch (error) { this.log("warn", `Error disposing texture: ${url}`, error); } }); this.textures.clear(); this.clearAllTimeouts(); this.clearAllIntervals(); if (this.shaderManager) { this.filters.forEach((filter) => { this.shaderManager?.releaseShader(filter); }); this.shaderManager = null; } if (this.metrics) { this.recordMetric("dispose", startTime); this.log("info", "Performance metrics summary:", this.metrics); } this.log("info", "All resources disposed"); } /** * Clears any pending updates in the queue */ clearPendingUpdates() { this.timeouts.forEach(clearTimeout); this.timeouts.clear(); this.intervals.forEach(clearInterval); this.intervals.clear(); } /** * Monitor filter performance and adjust quality if necessary * Part of the shader optimization implementation * * @param filter - Filter to monitor * @param performanceThreshold - Performance threshold in ms * @param qualityReduceFactor - How much to reduce quality (0-1) * @returns Whether the filter was optimized */ monitorFilterPerformance(filter, performanceThreshold = 16, qualityReduceFactor = 0.5) { if (!filter || !this.isActive()) return false; try { const startTime = performance.now(); if (filter.enabled) { if ("apply" in filter && typeof filter.apply === "function") { this.log("debug", "Monitoring filter performance"); } } const endTime = performance.now(); const renderTime = endTime - startTime; if (renderTime > performanceThreshold) { this.optimizeFilterQuality(filter, qualityReduceFactor); this.log("info", `Filter performance optimized: ${renderTime.toFixed(2)}ms -> threshold: ${performanceThreshold}ms`); return true; } return false; } catch (error) { this.log("warn", "Error monitoring filter performance", error); return false; } } /** * Optimize a filter's quality settings to improve performance * * @param filter - Filter to optimize * @param factor - Factor to reduce quality by (0-1) * @returns Whether optimization was applied */ optimizeFilterQuality(filter, factor = 0.5) { if (!filter || !this.isActive()) return false; try { let optimized = false; if ("quality" in filter && typeof filter.quality === "number") { const newQuality = Math.max(1, Math.floor(filter.quality * factor)); if (newQuality < filter.quality) { filter.quality = newQuality; optimized = true; this.log("debug", `Reduced filter quality to ${newQuality}`); } } if ("resolution" in filter && typeof filter.resolution === "number") { const newResolution = Math.max(0.1, filter.resolution * factor); if (newResolution < filter.resolution) { filter.resolution = newResolution; optimized = true; this.log("debug", `Reduced filter resolution to ${newResolution.toFixed(2)}`); } } const specificParams = [ "kernelSize", "blur", "steps", "passes", "iterations", "sampleSize", "pixelSize", "blurX", "blurY" ]; for (const param of specificParams) { if (param in filter && typeof filter[param] === "number") { const currentValue = filter[param]; const newValue = Math.max(1, Math.floor(currentValue * factor)); if (newValue < currentValue) { filter[param] = newValue; optimized = true; this.log("debug", `Reduced filter ${param} to ${newValue}`); } } } return optimized; } catch (error) { this.log("warn", "Error optimizing filter quality", error); return false; } } /** * Run diagnostics on all active filters * Provides insights into filter performance * * @returns Diagnostic information about filters */ runFilterDiagnostics() { const results = { totalFilters: this.filters.size, filtersByType: {}, potentialOptimizations: 0 }; try { this.filters.forEach((filter) => { const filterType = filter.constructor.name; if (!results.filtersByType[filterType]) { results.filtersByType[filterType] = 0; } results.filtersByType[filterType]++; let canOptimize = false; if ("quality" in filter && typeof filter.quality === "number" && filter.quality > 1) { canOptimize = true; } if ("resolution" in filter && typeof filter.resolution === "number" && filter.resolution > 0.5) { canOptimize = true; } if (canOptimize) { results.potentialOptimizations++; } }); if (this.shaderManager) { results.shaderPoolStats = this.shaderManager.getStats(); } return results; } catch (error) { this.log("warn", "Error running filter diagnostics", error); return { error: "Failed to run diagnostics", totalFilters: this.filters.size }; } } /** * Automatically optimize all filters based on FPS * This method should be called periodically during animation * * @param currentFPS - Current FPS of the application * @param targetFPS - Target FPS to maintain * @param optimizationStep - How aggressive to optimize (0-1) * @returns Number of filters optimized */ autoOptimizeFilters(currentFPS, targetFPS = 55, optimizationStep = 0.8) { if (!this.isActive() || this.filters.size === 0) return 0; if (currentFPS >= targetFPS) return 0; try { let optimizedCount = 0; const fpsDifference = targetFPS - currentFPS; const optimizationFactor = Math.min(0.9, Math.max( 0.5, optimizationStep * (1 - currentFPS / targetFPS) )); this.log("info", `Auto-optimizing filters: FPS ${currentFPS.toFixed(1)}/${targetFPS}, factor: ${optimizationFactor.toFixed(2)}`); const filterEntries = Array.from(this.filters.entries()).map(([id, filter]) => { let estimatedCost = 1; if ("quality" in filter && typeof filter.quality === "number") { estimatedCost *= filter.quality; } if ("resolution" in filter && typeof filter.resolution === "number") { estimatedCost *= filter.resolution; } return { id, filter, cost: estimatedCost }; }).sort((a, b) => b.cost - a.cost); for (const { filter } of filterEntries) { if (optimizedCount >= 3) break; const wasOptimized = this.optimizeFilterQuality(filter, optimizationFactor); if (wasOptimized) { optimizedCount++; } } if (optimizedCount > 0) { this.log("info", `Optimized ${optimizedCount} filters to improve performance`); } return optimizedCount; } catch (error) { this.log("warn", "Error auto-optimizing filters", error); return 0; } } } export { ResourceManager as default }; //# sourceMappingURL=ResourceManager.js.map