kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
882 lines (879 loc) • 37.5 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useRef, useState, useEffect, useCallback } from 'react';
import styles from './KineticSlider.module.css.js';
import { Application, Container } from 'pixi.js';
import ResourceManager from './managers/ResourceManager.js';
import { AtlasManager } from './managers/AtlasManager.js';
import { RenderScheduler } from './managers/RenderScheduler.js';
import { UpdateType } from './managers/UpdateTypes.js';
import { ThrottleStrategy } from './managers/FrameThrottler.js';
import { AnimationCoordinator } from './managers/AnimationCoordinator.js';
import SlidingWindowManager from './managers/SlidingWindowManager.js';
import 'pixi-filters';
import './filters/advancedBloomFilter.js';
import './filters/blurFilter.js';
import './filters/colorMatrixFilter.js';
import './filters/dropShadowFilter.js';
import 'gsap';
import './filters/tiltShiftFilter.js';
import { FilterFactory } from './filters/FilterFactory.js';
import { useDisplacementEffects } from './hooks/useDisplacementEffects.js';
import { useFilters } from './hooks/useFilters.js';
import { useSlides } from './hooks/useSlides.js';
import useTextContainers from './hooks/useTextContainers.js';
import useMouseTracking from './hooks/useMouseTracking.js';
import useIdleTimer from './hooks/useIdleTimer.js';
import useNavigation from './hooks/useNavigation.js';
import useExternalNav from './hooks/useExternalNav.js';
import useTouchSwipe from './hooks/useTouchSwipe.js';
import useMouseDrag from './hooks/useMouseDrag.js';
import useTextTilt from './hooks/useTextTilt.js';
import useResizeHandler from './hooks/useResizeHandler.js';
import { loadKineticSliderDependencies } from './ImportHelpers.js';
import { preloadKineticSliderAssets } from './utils/assetPreload.js';
// Define the filter coordination event name
const FILTER_COORDINATION_EVENT = 'kinetic-slider:filter-update';
/**
* Creates an interactive image slider with various displacement and transition effects
*/
const KineticSlider = ({
// Content sources
images = [], texts = [], slidesBasePath = '/images/',
// Displacement settings
backgroundDisplacementSpriteLocation = '/images/background-displace.jpg', cursorDisplacementSpriteLocation = '/images/cursor-displace.png', cursorImgEffect = true, cursorTextEffect = true, cursorScaleIntensity = 0.65, cursorMomentum = 0.14,
// Cursor displacement sizing options
cursorDisplacementSizing = 'natural', cursorDisplacementWidth, cursorDisplacementHeight,
// Text styling
textTitleColor = 'white', textTitleSize = 64, mobileTextTitleSize = 40, textTitleLetterspacing = 2, textTitleFontFamily, textSubTitleColor = 'white', textSubTitleSize = 24, mobileTextSubTitleSize = 18, textSubTitleLetterspacing = 1, textSubTitleOffsetTop = 10, mobileTextSubTitleOffsetTop = 5, textSubTitleFontFamily,
// Animation settings
maxContainerShiftFraction = 0.05, swipeScaleIntensity = 2, transitionScaleIntensity = 30,
// Navigation settings
externalNav = false, navElement = { prev: '.main-nav.prev', next: '.main-nav.next' }, buttonMode = false,
// Filter configurations
imageFilters, textFilters,
// Atlas configuration
slidesAtlas = 'slides-atlas', effectsAtlas = 'effects-atlas', useEffectsAtlas = false, useSlidesAtlas = false }) => {
// Debug log the props
console.log("KineticSlider received props:", {
useSlidesAtlas,
useEffectsAtlas,
slidesAtlas,
effectsAtlas
});
// Core references
const sliderRef = useRef(null);
const [isClient, setIsClient] = useState(false);
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const [isInteracting, setIsInteracting] = useState(false);
const [isAppReady, setIsAppReady] = useState(false);
const [assetsLoaded, setAssetsLoaded] = useState(false);
const cursorActiveRef = useRef(false);
// Initialize the render scheduler (singleton)
const scheduler = RenderScheduler.getInstance();
// Create ResourceManager instance with unique ID
const resourceManagerRef = useRef(null);
// Create SlidingWindowManager reference
const slidingWindowManagerRef = useRef(null);
// Initialize the animation coordinator and set the resource manager
const animationCoordinator = AnimationCoordinator.getInstance();
// Configure the scheduler for optimal performance
useEffect(() => {
// Only run on client-side
if (typeof window === 'undefined')
return;
scheduler.configureThrottling({
targetFps: 60,
strategy: ThrottleStrategy.PRIORITY
});
return () => {
// Clear any pending updates related to this component when unmounting
scheduler.clearQueue();
};
}, [scheduler]);
// Create AtlasManager instance
const atlasManagerRef = useRef(null);
// Set up Pixi app
const appRef = useRef(null);
const slidesRef = useRef([]);
const textContainersRef = useRef([]);
const backgroundDisplacementSpriteRef = useRef(null);
const cursorDisplacementSpriteRef = useRef(null);
const bgDispFilterRef = useRef(null);
const cursorDispFilterRef = useRef(null);
const currentIndexRef = useRef(0);
// Client-side initialization
useEffect(() => {
setIsClient(true);
}, []);
// Initialize ResourceManager and AtlasManager on mount
useEffect(() => {
const componentId = `kinetic-slider-${Math.random().toString(36).substring(2, 9)}`;
// Initialize ResourceManager
resourceManagerRef.current = new ResourceManager(componentId, {
enableMetrics: true,
enableShaderPooling: true,
logLevel: 'warn'
});
// Initialize FilterFactory with lazy loading
FilterFactory.initialize({
enableShaderPooling: true,
enableDebug: "production" === 'development',
lazyLoadConfig: {
unloadTimeoutMs: 120000, // 2 minutes
maxCachedModules: 20,
enablePrefetching: true,
retryFailedLoads: true,
maxRetries: 3
}
});
// Initialize AtlasManager with resource manager
atlasManagerRef.current = new AtlasManager({
debug: true,
preferAtlas: true,
cacheFrameTextures: true,
basePath: '/atlas'
}, resourceManagerRef.current);
// Set the resource manager for the animation coordinator
if (resourceManagerRef.current) {
animationCoordinator.setResourceManager(resourceManagerRef.current);
}
// Initialize SlidingWindowManager with default window size and initial index
slidingWindowManagerRef.current = new SlidingWindowManager({
totalSlides: images.length,
initialIndex: 0,
windowSize: 2, // Default: current slide ±2
debug: "production" === 'development'
}, resourceManagerRef.current);
return () => {
// Mark as unmounting to prevent new resource allocation
if (resourceManagerRef.current) {
console.log("Component unmounting - disposing all resources");
resourceManagerRef.current.markUnmounting();
// Dispose all tracked resources
resourceManagerRef.current.dispose();
resourceManagerRef.current = null;
}
// Clean up atlas manager
if (atlasManagerRef.current) {
atlasManagerRef.current.dispose();
atlasManagerRef.current = null;
}
// Clear the sliding window manager
slidingWindowManagerRef.current = null;
};
}, [images.length, animationCoordinator]);
// Create a pixi refs object for hooks
const pixiRefs = {
app: appRef,
slides: slidesRef,
textContainers: textContainersRef,
backgroundDisplacementSprite: backgroundDisplacementSpriteRef,
cursorDisplacementSprite: cursorDisplacementSpriteRef,
bgDispFilter: bgDispFilterRef,
cursorDispFilter: cursorDispFilterRef,
currentIndex: currentIndexRef
};
// Props object for hooks
const hookProps = {
images,
texts,
slidesBasePath,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
cursorImgEffect,
cursorTextEffect,
cursorScaleIntensity,
cursorMomentum,
cursorDisplacementSizing,
cursorDisplacementWidth,
cursorDisplacementHeight,
textTitleColor,
textTitleSize,
mobileTextTitleSize,
textTitleLetterspacing,
textTitleFontFamily,
textSubTitleColor,
textSubTitleSize,
mobileTextSubTitleSize,
textSubTitleLetterspacing,
textSubTitleOffsetTop,
mobileTextSubTitleOffsetTop,
textSubTitleFontFamily,
maxContainerShiftFraction,
swipeScaleIntensity,
transitionScaleIntensity,
imageFilters,
textFilters,
slidesAtlas,
effectsAtlas,
useSlidesAtlas,
useEffectsAtlas
};
// Enhanced hook params with resource and atlas managers
const hookParams = {
sliderRef,
pixi: pixiRefs,
props: hookProps,
resourceManager: resourceManagerRef.current,
atlasManager: atlasManagerRef.current,
slidingWindowManager: slidingWindowManagerRef.current
};
// Use displacement effects
const { showDisplacementEffects, hideDisplacementEffects } = useDisplacementEffects({
sliderRef,
bgDispFilterRef,
cursorDispFilterRef,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
appRef,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
cursorImgEffect,
cursorScaleIntensity,
cursorDisplacementSizing,
cursorDisplacementWidth,
cursorDisplacementHeight,
resourceManager: resourceManagerRef.current,
atlasManager: atlasManagerRef.current || undefined,
effectsAtlas,
useEffectsAtlas
});
// Use filter effects
const { updateFilterIntensities, resetAllFilters, activateFilterEffects, isInitialized: filtersInitialized, isActive: filtersActive, setFiltersActive } = useFilters(hookParams);
// Coordinate filter states with interaction state
useEffect(() => {
if (!isAppReady || !assetsLoaded) {
console.log("Skipping filter coordination - not ready", {
filtersInitialized,
isAppReady,
assetsLoaded
});
return;
}
// Try to initialize filters if they're not already initialized
if (!filtersInitialized && typeof activateFilterEffects === 'function') {
console.log("Attempting to initialize filters during coordination");
activateFilterEffects();
}
console.log(`Filter coordination - isInteracting: ${isInteracting}, filtersActive: ${filtersActive}`);
if (isInteracting) {
// When interaction starts, activate both displacement and custom filters
console.log("Interaction started - activating effects");
showDisplacementEffects();
// First try to use the dedicated activation function
if (typeof activateFilterEffects === 'function') {
console.log("Using activateFilterEffects");
activateFilterEffects();
}
else {
// Fallback to updateFilterIntensities
console.log("Using updateFilterIntensities");
updateFilterIntensities(true, true);
}
}
else {
// When interaction ends, deactivate both
console.log("Interaction ended - deactivating effects");
hideDisplacementEffects();
updateFilterIntensities(false);
}
}, [
isInteracting,
isAppReady,
assetsLoaded,
filtersInitialized,
filtersActive,
showDisplacementEffects,
hideDisplacementEffects,
updateFilterIntensities,
activateFilterEffects
]);
// Reset filters when component unmounts or when app/assets state changes
useEffect(() => {
if (!isAppReady || !assetsLoaded) {
resetAllFilters();
}
return () => {
resetAllFilters();
};
}, [isAppReady, assetsLoaded, resetAllFilters]);
// Preload assets including fonts and atlases
useEffect(() => {
if (typeof window === 'undefined' || !isClient)
return;
const loadAssets = async () => {
try {
console.log("Preloading assets and fonts...");
// Preload atlases first
if (atlasManagerRef.current) {
// Load the slides atlas
if (slidesAtlas) {
await atlasManagerRef.current.loadAtlas(slidesAtlas, `/atlas/${slidesAtlas}.json`, `/atlas/${slidesAtlas}.png`);
}
// Load the effects atlas
if (effectsAtlas) {
await atlasManagerRef.current.loadAtlas(effectsAtlas, `/atlas/${effectsAtlas}.json`, `/atlas/${effectsAtlas}.png`);
}
}
// Then preload any remaining assets (as fallback)
await preloadKineticSliderAssets(images, backgroundDisplacementSpriteLocation, cursorDisplacementSpriteLocation, textTitleFontFamily, textSubTitleFontFamily);
setAssetsLoaded(true);
console.log("Assets and fonts preloaded successfully");
}
catch (error) {
console.error("Failed to preload assets:", error);
// Continue anyway so the component doesn't totally fail
setAssetsLoaded(true);
}
};
loadAssets();
}, [
isClient,
images,
backgroundDisplacementSpriteLocation,
cursorDisplacementSpriteLocation,
textTitleFontFamily,
textSubTitleFontFamily,
slidesAtlas,
effectsAtlas
]);
// Initialize Pixi.js application
useEffect(() => {
if (typeof window === 'undefined' || !sliderRef.current || appRef.current || !assetsLoaded)
return;
const initPixi = async () => {
try {
console.log("Loading PixiJS dependencies...");
// Load all dependencies first
const { gsap, pixi, pixiPlugin } = await loadKineticSliderDependencies();
// Only register plugins in browser
if (typeof window !== 'undefined' && pixiPlugin) {
// Register GSAP plugins
gsap.registerPlugin(pixiPlugin);
// Check if we have the actual plugin (not the mock)
if (pixiPlugin.registerPIXI) {
pixiPlugin.registerPIXI(pixi);
}
}
console.log("Creating Pixi.js application...");
// Create Pixi application
const app = new Application();
await app.init({
width: sliderRef.current?.clientWidth || 800,
height: sliderRef.current?.clientHeight || 600,
backgroundAlpha: 0,
resizeTo: sliderRef.current || undefined,
});
// Track the application with the resource manager
if (resourceManagerRef.current) {
resourceManagerRef.current.trackPixiApp(app);
}
// Add canvas to DOM
if (sliderRef.current) {
sliderRef.current.appendChild(app.canvas);
}
// Store reference
appRef.current = app;
// Create main container
const stage = new Container();
app.stage.addChild(stage);
// Track the stage with the resource manager
if (resourceManagerRef.current) {
resourceManagerRef.current.trackDisplayObject(stage);
}
// Set app as ready
setIsAppReady(true);
console.log("Pixi.js application initialized");
}
catch (error) {
console.error("Failed to initialize Pixi.js application:", error);
}
};
initPixi();
// Cleanup on unmount
return () => {
if (appRef.current) {
if (sliderRef.current) {
const canvas = sliderRef.current.querySelector('canvas');
if (canvas) {
sliderRef.current.removeChild(canvas);
}
}
// Note: We don't need to manually destroy the app here
// as the ResourceManager will handle it during disposal
appRef.current = null;
setIsAppReady(false);
}
};
}, [sliderRef.current, assetsLoaded]);
// Prefetch filters based on props
useEffect(() => {
// Only run on client-side after app is initialized and props are loaded
if (typeof window === 'undefined' || !isAppReady)
return;
// Skip if no filter configurations
if (!hookProps.imageFilters && !hookProps.textFilters)
return;
// Helper to extract filter types from configurations
const getFilterTypes = (config) => {
if (!config)
return [];
const configs = Array.isArray(config) ? config : [config];
return configs
.filter(c => c && c.type && c.enabled !== false)
.map(c => c.type);
};
// Get unique filter types from both image and text filters
const imageFilterTypes = getFilterTypes(hookProps.imageFilters);
const textFilterTypes = getFilterTypes(hookProps.textFilters);
const allFilterTypes = [...new Set([...imageFilterTypes, ...textFilterTypes])];
// Prefetch all filter types
if (allFilterTypes.length > 0) {
console.log(`Prefetching ${allFilterTypes.length} filter types:`, allFilterTypes);
FilterFactory.prefetchFilterModules(allFilterTypes, 'high');
}
}, [isAppReady, hookProps.imageFilters, hookProps.textFilters]);
// Handle slide transitions
const handleSlideChange = useCallback((newIndex) => {
// Update the current index
currentIndexRef.current = newIndex;
setCurrentSlideIndex(newIndex);
// Activate filters for the new slide
if (filtersInitialized) {
// If cursor is within the canvas, apply filters immediately with critical priority
if (isInteracting) {
console.log("Cursor is within canvas - applying filters immediately with critical priority");
// Schedule displacement effects with critical priority
scheduler.scheduleTypedUpdate('slider', UpdateType.DISPLACEMENT_EFFECT, () => {
showDisplacementEffects();
}, 'critical');
// Schedule filter activation with critical priority
// Force the filter activation to use the new slide index
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
// Explicitly update the current index ref before activating filters
currentIndexRef.current = newIndex;
// Force filter activation for the new slide
if (typeof activateFilterEffects === 'function') {
activateFilterEffects();
// Double-check that filters are active
setFiltersActive(true);
}
}, 'critical');
}
else {
// Use activateFilterEffects to properly apply filters to the new slide
activateFilterEffects();
// Schedule filter deactivation if not interacting
const transitionDuration = 1000; // 1 second transition
setTimeout(() => {
if (!isInteracting) { // Check again in case interaction started during transition
updateFilterIntensities(false);
hideDisplacementEffects();
}
}, transitionDuration);
}
}
}, [
filtersInitialized,
isInteracting,
activateFilterEffects,
updateFilterIntensities,
hideDisplacementEffects,
showDisplacementEffects,
scheduler,
setFiltersActive
]);
// Use slides hook with transition handler
const { nextSlide, prevSlide } = useSlides({
...hookParams,
onSlideChange: handleSlideChange
});
// Use text containers
useTextContainers({
sliderRef,
appRef,
slidesRef,
textContainersRef,
currentIndex: currentIndexRef,
buttonMode,
texts,
textTitleColor,
textTitleSize,
mobileTextTitleSize,
textTitleLetterspacing,
textTitleFontFamily,
textSubTitleColor,
textSubTitleSize,
mobileTextSubTitleSize,
textSubTitleLetterspacing,
textSubTitleOffsetTop,
mobileTextSubTitleOffsetTop,
textSubTitleFontFamily,
resourceManager: resourceManagerRef.current
});
// Use mouse tracking
useMouseTracking({
...hookParams,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef,
cursorImgEffect,
cursorMomentum
});
// Use idle timer
useIdleTimer({
sliderRef,
cursorActive: cursorActiveRef,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect,
defaultBgFilterScale: 20,
defaultCursorFilterScale: 10,
resourceManager: resourceManagerRef.current
});
/**
* Handles transition to the next slide
* Updates state and reapplies effects after transition
*/
const handleNext = useCallback(() => {
if (!appRef.current || !isAppReady || slidesRef.current.length === 0)
return;
const nextIndex = (currentSlideIndex + 1) % slidesRef.current.length;
// Schedule slide transition with high priority
scheduler.scheduleTypedUpdate('slider', UpdateType.SLIDE_TRANSITION, () => {
// First transition the slide
nextSlide(nextIndex);
// Update the current index
currentIndexRef.current = nextIndex;
setCurrentSlideIndex(nextIndex);
// Always schedule effect reapplication after a short delay
// regardless of whether we're interacting or not
setTimeout(() => {
console.log("Scheduling effects after slide change (next)");
// Schedule displacement effects reapplication
scheduler.scheduleTypedUpdate('slider', UpdateType.DISPLACEMENT_EFFECT, () => {
showDisplacementEffects();
},
// Use critical priority if cursor is within the canvas
isInteracting ? 'critical' : undefined);
// Schedule filter update with appropriate priority based on interaction state
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
// Ensure current index is set correctly
currentIndexRef.current = nextIndex;
// Use activateFilterEffects to properly apply filters to the new slide
if (typeof activateFilterEffects === 'function') {
activateFilterEffects();
// If cursor is within canvas, ensure filters are active
if (isInteracting) {
setFiltersActive(true);
}
}
},
// Use critical priority if cursor is within the canvas
isInteracting ? 'critical' : undefined);
}, 100); // Short delay to allow transition to start
});
}, [
appRef,
isAppReady,
slidesRef,
currentSlideIndex,
nextSlide,
isInteracting,
showDisplacementEffects,
activateFilterEffects,
scheduler,
setFiltersActive
]);
/**
* Handles transition to the previous slide
* Updates state and reapplies effects after transition
*/
const handlePrev = useCallback(() => {
if (!appRef.current || !isAppReady || slidesRef.current.length === 0)
return;
const prevIndex = (currentSlideIndex - 1 + slidesRef.current.length) % slidesRef.current.length;
// Schedule slide transition with high priority
scheduler.scheduleTypedUpdate('slider', UpdateType.SLIDE_TRANSITION, () => {
// First transition the slide
prevSlide(prevIndex);
// Update the current index
currentIndexRef.current = prevIndex;
setCurrentSlideIndex(prevIndex);
// Always schedule effect reapplication after a short delay
// regardless of whether we're interacting or not
setTimeout(() => {
console.log("Scheduling effects after slide change (prev)");
// Schedule displacement effects reapplication
scheduler.scheduleTypedUpdate('slider', UpdateType.DISPLACEMENT_EFFECT, () => {
showDisplacementEffects();
},
// Use critical priority if cursor is within the canvas
isInteracting ? 'critical' : undefined);
// Schedule filter update with appropriate priority based on interaction state
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
// Ensure current index is set correctly
currentIndexRef.current = prevIndex;
// Use activateFilterEffects to properly apply filters to the new slide
if (typeof activateFilterEffects === 'function') {
activateFilterEffects();
// If cursor is within canvas, ensure filters are active
if (isInteracting) {
setFiltersActive(true);
}
}
},
// Use critical priority if cursor is within the canvas
isInteracting ? 'critical' : undefined);
}, 100); // Short delay to allow transition to start
});
}, [
appRef,
isAppReady,
slidesRef,
currentSlideIndex,
prevSlide,
isInteracting,
showDisplacementEffects,
activateFilterEffects,
scheduler,
setFiltersActive
]);
// Apply hooks only when appRef is available and ready - NOW updateFilterIntensities is defined before being referenced
useEffect(() => {
// Skip if app is not initialized
if (!appRef.current || !isAppReady)
return;
// Update current index ref when state changes
currentIndexRef.current = currentSlideIndex;
// Note: We no longer need to handle filter updates here as they are now handled directly
// in the navigation functions (handleNext/handlePrev)
}, [appRef.current, currentSlideIndex, isAppReady]);
// Use navigation
useNavigation({
onNext: handleNext,
onPrev: handlePrev,
enableKeyboardNav: true
});
// Use external navigation if enabled
useExternalNav({
externalNav,
navElement,
handleNext,
handlePrev
});
// Use touch swipe
useTouchSwipe({
sliderRef,
onSwipeLeft: handleNext,
onSwipeRight: handlePrev
});
// Use mouse drag
useMouseDrag({
sliderRef,
slidesRef,
currentIndex: currentIndexRef,
swipeScaleIntensity,
swipeDistance: typeof window !== 'undefined' ? window.innerWidth * 0.2 : 200,
onSwipeLeft: handleNext,
onSwipeRight: handlePrev
});
// Use text tilt
useTextTilt({
sliderRef,
textContainersRef,
currentIndex: currentIndexRef,
cursorTextEffect,
maxContainerShiftFraction,
bgDispFilterRef,
cursorDispFilterRef,
cursorImgEffect
});
// Use resize handler
useResizeHandler({
sliderRef,
appRef,
slidesRef,
textContainersRef,
backgroundDisplacementSpriteRef,
cursorDisplacementSpriteRef
});
/**
* Handles mouse enter events on the slider element
* Activates displacement effects and filter intensities
* Now uses scheduler for batched updates
*/
const handleMouseEnter = useCallback(() => {
if (!isAppReady)
return;
console.log("Mouse entered the slider - activating effects immediately", {
filtersInitialized,
filtersActive,
hasActivateFunction: typeof activateFilterEffects === 'function',
hasUpdateFunction: typeof updateFilterIntensities === 'function',
currentSlideIndex
});
// Update cursor active state
cursorActiveRef.current = true;
// Set interaction state immediately to ensure proper coordination
setIsInteracting(true);
// Schedule displacement effects with critical priority
scheduler.scheduleTypedUpdate('slider', UpdateType.DISPLACEMENT_EFFECT, () => {
// Apply displacement effects immediately
showDisplacementEffects();
console.log("Displacement effects activated");
}, 'critical');
// Schedule filter activation with critical priority
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
// Force initialize filters if not already initialized
if (!filtersInitialized && typeof activateFilterEffects === 'function') {
console.log("Filters not initialized, initializing now");
activateFilterEffects();
}
else if (typeof activateFilterEffects === 'function') {
// Use the dedicated activation function as the primary method
console.log("Using activateFilterEffects function for slide", currentSlideIndex);
activateFilterEffects();
}
else if (typeof updateFilterIntensities === 'function') {
// Only use updateFilterIntensities as fallback
console.log("Using updateFilterIntensities function with force=true");
updateFilterIntensities(true, true);
}
// Explicitly set filters as active
setFiltersActive(true);
console.log("Filter activation completed with critical priority");
}, 'critical');
// Schedule a final render update to ensure all changes are applied
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
console.log("Final render update after mouse enter completed");
// Double-check filter state after render
console.log("Filter state after render:", {
filtersActive,
bgDispFilterEnabled: bgDispFilterRef.current?.enabled,
cursorDispFilterEnabled: cursorDispFilterRef.current?.enabled,
currentSlideIndex
});
}, 'critical');
}, [
isAppReady,
showDisplacementEffects,
updateFilterIntensities,
activateFilterEffects,
filtersInitialized,
filtersActive,
setFiltersActive,
currentSlideIndex,
scheduler
]);
/**
* Handles mouse leave event
*/
const handleMouseLeave = useCallback(() => {
if (!isAppReady)
return;
// Set interaction state immediately
setIsInteracting(false);
// Explicitly dispatch filter coordination events for both displacement filters
const event1 = new CustomEvent(FILTER_COORDINATION_EVENT, {
detail: {
type: 'background-displacement',
intensity: 0,
timestamp: Date.now(),
source: 'slider-component',
priority: 'critical'
}
});
window.dispatchEvent(event1);
const event2 = new CustomEvent(FILTER_COORDINATION_EVENT, {
detail: {
type: 'cursor-displacement',
intensity: 0,
timestamp: Date.now(),
source: 'slider-component',
priority: 'critical'
}
});
window.dispatchEvent(event2);
// Force the filters to deactivate with critical priority
updateFilterIntensities(false, true);
// Schedule a final update after mouse leave to ensure everything is cleaned up
scheduler.scheduleTypedUpdate('slider', UpdateType.FILTER_UPDATE, () => {
}, 'critical');
}, [isAppReady, setIsInteracting, updateFilterIntensities, scheduler]);
// Handle component cleanup
useEffect(() => {
return () => {
// Reset all filters
resetAllFilters();
// Clear any pending filter updates
if (resourceManagerRef.current) {
resourceManagerRef.current.clearPendingUpdates();
}
// Clear any scheduled filter transitions
const transitionTimeouts = Array.from(document.querySelectorAll(`[data-slider-id="${sliderRef.current?.id}"]`))
.map(el => parseInt(el.getAttribute('data-timeout-id') || '0'))
.filter(id => id > 0);
transitionTimeouts.forEach(clearTimeout);
};
}, [resetAllFilters]);
// Error boundary for filter operations
useEffect(() => {
const handleError = (error) => {
console.error('Filter system error:', error);
// Attempt recovery by resetting filters
resetAllFilters();
// Hide all effects
hideDisplacementEffects();
};
window.addEventListener('error', (e) => handleError(e.error));
return () => window.removeEventListener('error', (e) => handleError(e.error));
}, [resetAllFilters, hideDisplacementEffects]);
// Add a new useEffect to handle cleanup
useEffect(() => {
return () => {
// Clean up any resources when component unmounts
if (resourceManagerRef.current) {
// Use the existing methods instead of the non-existent 'cleanup' method
resourceManagerRef.current.clearPendingUpdates();
resourceManagerRef.current.markUnmounting();
}
};
}, []);
// FPS monitoring for filter optimization
useEffect(() => {
if (!resourceManagerRef.current)
return;
let frameCount = 0;
let lastTime = performance.now();
let fps = 60;
// Function to calculate FPS and optimize filters
const monitorPerformance = () => {
frameCount++;
const currentTime = performance.now();
const elapsed = currentTime - lastTime;
// Update FPS approximately every second
if (elapsed > 1000) {
fps = (frameCount * 1000) / elapsed;
frameCount = 0;
lastTime = currentTime;
// Auto-optimize filters based on current FPS
if (resourceManagerRef.current && fps < 55) {
resourceManagerRef.current.autoOptimizeFilters(fps, 55);
}
}
performanceMonitorId = requestAnimationFrame(monitorPerformance);
};
// Start performance monitoring
let performanceMonitorId = requestAnimationFrame(monitorPerformance);
// Cleanup
return () => {
if (performanceMonitorId) {
cancelAnimationFrame(performanceMonitorId);
}
};
}, [resourceManagerRef]);
// Render component
return (jsxs("div", { className: styles.kineticSlider, ref: sliderRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [(!isAppReady || !assetsLoaded) && (jsx("div", { className: styles.placeholder, children: jsxs("div", { className: styles.loadingIndicator, children: [jsx("div", { className: styles.spinner }), jsx("div", { children: "Loading slider..." })] }) })), !externalNav && isClient && (jsxs("nav", { children: [jsx("button", { onClick: handlePrev, className: styles.prev, children: "Prev" }), jsx("button", { onClick: handleNext, className: styles.next, children: "Next" })] }))] }));
};
export { KineticSlider as default };
//# sourceMappingURL=KineticSlider.js.map