kinetic-slider
Version:
A WebGL-powered kinetic slider component using PIXI.js
1 lines • 72.9 kB
Source Map (JSON)
{"version":3,"file":"useSlides.cjs","sources":["../../../src/hooks/useSlides.ts"],"sourcesContent":["import { useEffect, useCallback, useState, useRef } from 'react';\nimport { Sprite, Container, Assets, Texture } from 'pixi.js';\nimport { type EnhancedSprite, type HookParams } from '../types';\nimport { calculateSpriteScale } from '../utils/calculateSpriteScale';\nimport { gsap } from 'gsap';\nimport ResourceManager from '../managers/ResourceManager';\nimport { AtlasManager } from '../managers/AtlasManager';\nimport AnimationCoordinator, { AnimationGroupType } from '../managers/AnimationCoordinator';\nimport SlidingWindowManager from '../managers/SlidingWindowManager';\n\n// Development environment check\nconst isDevelopment = import.meta.env?.MODE === 'development';\n\n// Interface for the hook's return value\ninterface UseSlidesResult {\n transitionToSlide: (nextIndex: number) => gsap.core.Timeline | null;\n nextSlide: (nextIndex: number) => void;\n prevSlide: (prevIndex: number) => void;\n isLoading: boolean;\n loadingProgress: number;\n}\n\n/**\n * Hook to create and manage slide sprites with atlas support\n */\nexport const useSlides = (\n { sliderRef, pixi, props, resourceManager, atlasManager, onSlideChange, slidingWindowManager }: HookParams & {\n resourceManager?: ResourceManager | null,\n atlasManager?: AtlasManager | null\n }\n): UseSlidesResult => {\n // Debug logging of props\n console.log(\"useSlides received useSlidesAtlas:\", props.useSlidesAtlas);\n console.log(\"useSlides received props:\", props);\n\n // Track loading state\n const [isLoading, setIsLoading] = useState(false);\n const [loadingProgress, setLoadingProgress] = useState(0);\n\n // Ref to store active transitions\n const activeTransitionRef = useRef<gsap.core.Timeline | null>(null);\n\n // Get the animation coordinator\n const animationCoordinator = AnimationCoordinator.getInstance();\n\n // Get the slidesBasePath from props, defaulting to '/images/' if not provided\n const slidesBasePath = props.slidesBasePath || '/images/';\n\n // Normalize path for atlas frame lookup\n const normalizePath = (imagePath: string): string => {\n // For paths that start with a slash, remove it for atlas lookup\n if (imagePath.startsWith('/')) {\n return imagePath.substring(1);\n }\n return imagePath;\n };\n\n // Helper to check if useSlidesAtlas is enabled (handling different possible values)\n const isUseSlidesAtlasEnabled = (): boolean => {\n // Handle all possible representations of \"true\"\n if (props.useSlidesAtlas === true) return true;\n if (typeof props.useSlidesAtlas === 'string' && props.useSlidesAtlas === 'true') return true;\n\n // Handle numeric representations (needs type checking)\n if (typeof props.useSlidesAtlas === 'number' && props.useSlidesAtlas === 1) return true;\n if (typeof props.useSlidesAtlas === 'string' && props.useSlidesAtlas === '1') return true;\n\n // Default to false for all other cases\n return false;\n };\n\n // Check if assets are available in atlas\n const areAssetsInAtlas = useCallback((): boolean => {\n // First check if atlasManager and slidesAtlas are available\n if (!atlasManager || !props.slidesAtlas) {\n if (isDevelopment) {\n console.log(`Atlas not available: atlasManager=${!!atlasManager}, slidesAtlas=${!!props.slidesAtlas}`);\n }\n return false;\n }\n\n // Check if useSlidesAtlas is enabled\n const useSlidesAtlasEnabled = isUseSlidesAtlasEnabled();\n if (isDevelopment) {\n console.log(`Atlas usage setting: useSlidesAtlas=${props.useSlidesAtlas}, enabled=${useSlidesAtlasEnabled}`);\n }\n\n if (!useSlidesAtlasEnabled) {\n if (isDevelopment) {\n console.log(`Atlas usage is disabled by useSlidesAtlas setting: ${props.useSlidesAtlas}`);\n }\n return false;\n }\n\n // Check if all images are in the atlas\n const missingFrames: string[] = [];\n const result = props.images.every(imagePath => {\n const normalizedPath = normalizePath(imagePath);\n\n if (isDevelopment) {\n console.log(`Checking if atlas has frame: \"${normalizedPath}\"`);\n }\n\n const atlasId = atlasManager.hasFrame(normalizedPath);\n\n if (!atlasId && isDevelopment) {\n missingFrames.push(normalizedPath);\n }\n\n return !!atlasId;\n });\n\n // In development mode, log which frames are missing if any\n if (isDevelopment && missingFrames.length > 0) {\n console.warn(`[KineticSlider] The following frames are missing from atlas: ${missingFrames.join(', ')}`);\n }\n\n return result;\n }, [atlasManager, props.images, props.slidesAtlas, props.useSlidesAtlas]);\n\n // Effect to create slides from atlas or individual images\n useEffect(() => {\n if (!pixi.app.current || !pixi.app.current.stage) {\n if (isDevelopment) {\n console.log(\"App or stage not available for slides, deferring initialization\");\n }\n return;\n }\n\n // Check if we have images to display\n if (!props.images.length) {\n if (isDevelopment) {\n console.warn(\"No images provided for slides\");\n }\n return;\n }\n\n // Check if slider ref is available for dimensions\n if (!sliderRef.current) {\n if (isDevelopment) {\n console.warn(\"Slider reference not available, deferring slide creation\");\n }\n return;\n }\n\n // Create a dedicated container for slides if it doesn't exist\n let slidesContainer: Container;\n try {\n const app = pixi.app.current;\n\n if (app.stage.children.length > 0 && app.stage.children[0] instanceof Container) {\n slidesContainer = app.stage.children[0] as Container;\n } else {\n slidesContainer = new Container();\n slidesContainer.label = 'slidesContainer';\n app.stage.addChild(slidesContainer);\n\n // Track container with resource manager if available\n if (resourceManager) {\n resourceManager.trackDisplayObject(slidesContainer);\n }\n }\n\n // Clear existing slides with proper cleanup\n pixi.slides.current.forEach(sprite => {\n if (sprite && sprite.parent) {\n try {\n sprite.parent.removeChild(sprite);\n } catch (error) {\n if (isDevelopment) {\n console.warn('Error removing sprite from parent:', error);\n }\n }\n }\n });\n pixi.slides.current = [];\n\n // Enhanced handling of the useAtlas decision\n const useSlidesAtlasEnabled = isUseSlidesAtlasEnabled();\n const useAtlas = atlasManager && props.slidesAtlas && areAssetsInAtlas() && useSlidesAtlasEnabled;\n\n if (isDevelopment) {\n if (useAtlas) {\n console.log(`%c[KineticSlider] Using texture atlas: ${props.slidesAtlas} for ${props.images.length} slides`, 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 3px;');\n } else {\n const reason = !atlasManager\n ? \"AtlasManager not available\"\n : !props.slidesAtlas\n ? \"No slidesAtlas property specified\"\n : !useSlidesAtlasEnabled\n ? `Atlas usage disabled by useSlidesAtlas=${props.useSlidesAtlas}`\n : \"Not all images found in atlas\";\n console.log(`%c[KineticSlider] Using individual images (${reason})`, 'background: #FFA726; color: white; padding: 2px 5px; border-radius: 3px;');\n }\n }\n\n if (useAtlas) {\n loadSlidesFromAtlas(slidesContainer);\n } else {\n loadSlidesFromIndividualImages(slidesContainer);\n }\n } catch (error) {\n if (isDevelopment) {\n console.error(\"Error setting up slides container:\", error);\n }\n setIsLoading(false);\n }\n }, [pixi.app.current, props.images, resourceManager, sliderRef, atlasManager, props.slidesAtlas, props.useSlidesAtlas]);\n\n /**\n * Load slides from texture atlas\n */\n const loadSlidesFromAtlas = async (slidesContainer: Container) => {\n if (!pixi.app.current || !sliderRef.current || !atlasManager) return;\n\n try {\n setIsLoading(true);\n setLoadingProgress(0);\n\n if (isDevelopment) {\n console.log(`%c[KineticSlider] Loading ${props.images.length} slide images from atlas: ${props.slidesAtlas}`, 'color: #2196F3');\n\n if (slidingWindowManager) {\n console.log(`%c[KineticSlider] Using sliding window approach with window size ±${slidingWindowManager.getWindowSize()}`, 'color: #4CAF50');\n }\n }\n\n const app = pixi.app.current;\n const sliderWidth = sliderRef.current.clientWidth;\n const sliderHeight = sliderRef.current.clientHeight;\n\n // Prepare for loading from atlas\n const totalImages = props.images.length;\n let loadedCount = 0;\n\n // Get visibility window indices if sliding window manager is available\n const visibilityWindowIndices = slidingWindowManager\n ? slidingWindowManager.getWindowIndices()\n : [];\n\n if (isDevelopment && slidingWindowManager) {\n console.log(`%c[KineticSlider] Visibility window: [${visibilityWindowIndices.join(', ')}]`, 'color: #4CAF50');\n }\n\n // Create sprites for each image using the atlas\n for (const [index, imagePath] of props.images.entries()) {\n try {\n // Check if this slide is in the visibility window\n const isInVisibilityWindow = !slidingWindowManager ||\n visibilityWindowIndices.includes(index);\n\n if (isDevelopment && slidingWindowManager) {\n console.log(`Slide ${index}: ${isInVisibilityWindow ? 'In visibility window' : 'Outside visibility window'}`);\n }\n\n if (isInVisibilityWindow) {\n // Fully load the slide if it's within the visibility window\n // Normalize path for atlas lookup\n const normalizedPath = normalizePath(imagePath);\n\n if (isDevelopment) {\n console.log(`Looking up atlas frame for normalized path: \"${normalizedPath}\"`);\n }\n\n // Get texture from atlas\n const texture = atlasManager.getFrameTexture(normalizedPath, props.slidesAtlas);\n\n if (!texture) {\n throw new Error(`Frame ${normalizedPath} not found in atlas ${props.slidesAtlas}`);\n }\n\n // Track texture with resource manager if available\n if (resourceManager) {\n resourceManager.trackTexture(imagePath, texture);\n }\n\n // Create the sprite with the texture from atlas\n const sprite = new Sprite(texture) as EnhancedSprite;\n sprite.anchor.set(0.5);\n sprite.x = app.screen.width / 2;\n sprite.y = app.screen.height / 2;\n\n // Set initial state - only show the first slide\n sprite.alpha = index === 0 ? 1 : 0;\n sprite.visible = index === 0;\n\n // Calculate and apply scale\n try {\n const { scale, baseScale } = calculateSpriteScale(\n texture.width,\n texture.height,\n sliderWidth,\n sliderHeight\n );\n\n sprite.scale.set(scale);\n sprite.baseScale = baseScale;\n } catch (scaleError) {\n if (isDevelopment) {\n console.warn(`Error calculating scale for slide ${index}:`, scaleError);\n }\n\n // Fallback scaling\n sprite.scale.set(1);\n sprite.baseScale = 1;\n }\n\n // Mark as being in the visibility window\n sprite._inVisibilityWindow = true;\n\n // Track the sprite with resource manager if available\n if (resourceManager) {\n resourceManager.trackDisplayObject(sprite);\n }\n\n // Add to container and store reference\n slidesContainer.addChild(sprite);\n pixi.slides.current.push(sprite);\n\n if (isDevelopment) {\n console.log(`Created full slide ${index} for ${imagePath} from atlas`);\n }\n } else {\n // Create a placeholder for slides outside the visibility window\n const placeholderOptions = {\n width: sliderWidth,\n height: sliderHeight,\n color: 0x333333,\n showIndex: isDevelopment, // Show index in development mode\n index,\n trackWithResourceManager: true,\n resourceManager,\n renderer: app.renderer\n };\n\n // Import the createPlaceholderSprite function dynamically to avoid circular dependencies\n const { createPlaceholderSprite } = await import('../utils/placeholderUtils');\n\n // Create the placeholder sprite\n const placeholderSprite = createPlaceholderSprite(placeholderOptions);\n\n // Position the placeholder at the center\n placeholderSprite.x = app.screen.width / 2;\n placeholderSprite.y = app.screen.height / 2;\n\n // Set initial state - only show the first slide\n placeholderSprite.alpha = index === 0 ? 1 : 0;\n placeholderSprite.visible = index === 0;\n\n // Set scale similar to real sprites\n placeholderSprite.scale.set(1);\n placeholderSprite.baseScale = 1;\n\n // Mark as placeholder and outside visibility window\n placeholderSprite._isPlaceholder = true;\n placeholderSprite._placeholderIndex = index;\n placeholderSprite._inVisibilityWindow = false;\n\n // Add to container and store reference\n slidesContainer.addChild(placeholderSprite);\n pixi.slides.current.push(placeholderSprite);\n\n if (isDevelopment) {\n console.log(`Created placeholder for slide ${index} (outside visibility window)`);\n }\n }\n\n // Update progress\n loadedCount++;\n const progress = (loadedCount / totalImages) * 100;\n setLoadingProgress(progress);\n\n } catch (error) {\n if (isDevelopment) {\n console.error(`Error creating slide for ${imagePath} from atlas:`, error);\n }\n // Fallback to individual image loading if atlas frame not found\n const texture = await Assets.load(imagePath);\n createSlideFromTexture(texture, imagePath, index, slidesContainer, app, sliderWidth, sliderHeight);\n\n // Update progress\n loadedCount++;\n setLoadingProgress((loadedCount / totalImages) * 100);\n }\n }\n\n setIsLoading(false);\n setLoadingProgress(100);\n\n if (isDevelopment) {\n console.log(`Finished loading ${loadedCount} slides from atlas`);\n }\n } catch (error) {\n if (isDevelopment) {\n console.error(\"Error loading slides from atlas:\", error);\n }\n // Fallback to individual image loading\n loadSlidesFromIndividualImages(slidesContainer);\n }\n };\n\n /**\n * Load slides from individual images (fallback method)\n */\n const loadSlidesFromIndividualImages = async (slidesContainer: Container) => {\n if (!pixi.app.current || !sliderRef.current) return;\n\n try {\n setIsLoading(true);\n setLoadingProgress(0);\n\n if (isDevelopment) {\n console.log(`%c[KineticSlider] Loading ${props.images.length} slide images individually (atlas not available or incomplete)`, 'color: #FF9800');\n\n if (slidingWindowManager) {\n console.log(`%c[KineticSlider] Using sliding window approach with window size ±${slidingWindowManager.getWindowSize()}`, 'color: #4CAF50');\n }\n }\n\n // Prepare the list of images to load\n const app = pixi.app.current;\n const sliderWidth = sliderRef.current.clientWidth;\n const sliderHeight = sliderRef.current.clientHeight;\n\n if (isDevelopment) {\n console.log(`Slider dimensions: ${sliderWidth}x${sliderHeight}`);\n }\n\n // Get visibility window indices if sliding window manager is available\n const visibilityWindowIndices = slidingWindowManager\n ? slidingWindowManager.getWindowIndices()\n : [];\n\n if (isDevelopment && slidingWindowManager) {\n console.log(`%c[KineticSlider] Visibility window: [${visibilityWindowIndices.join(', ')}]`, 'color: #4CAF50');\n }\n\n // Filter out images that are not in the visibility window\n const imagesToLoad = props.images\n .filter((_, index) => !slidingWindowManager || visibilityWindowIndices.includes(index))\n .filter(image => !Assets.cache.has(image));\n\n if (isDevelopment) {\n console.log(`Preparing to load ${imagesToLoad.length} images (${props.images.length - imagesToLoad.length} skipped or cached)`);\n if (slidingWindowManager) {\n console.log(`${props.images.length - visibilityWindowIndices.length} slides outside visibility window will use placeholders`);\n }\n }\n\n // Add assets to a bundle for batch loading and progress tracking\n if (imagesToLoad.length > 0) {\n // Create an assets bundle\n Assets.addBundle('slider-images', imagesToLoad.reduce((acc, image, index) => {\n acc[`slide-${index}`] = image;\n return acc;\n }, {} as Record<string, string>));\n\n // Load the bundle with progress tracking\n await Assets.loadBundle('slider-images', (progress) => {\n setLoadingProgress(progress * 100);\n });\n }\n\n // Create sprites for each image\n for (const [index, image] of props.images.entries()) {\n try {\n // Check if this slide is in the visibility window\n const isInVisibilityWindow = !slidingWindowManager ||\n visibilityWindowIndices.includes(index);\n\n if (isDevelopment && slidingWindowManager) {\n console.log(`Slide ${index}: ${isInVisibilityWindow ? 'In visibility window' : 'Outside visibility window'}`);\n }\n\n if (isInVisibilityWindow) {\n // Fully load the slide if it's within the visibility window\n // Get texture from cache\n const texture = Assets.get(image);\n\n // Create slide sprite\n const sprite = createSlideFromTexture(texture, image, index, slidesContainer, app, sliderWidth, sliderHeight);\n\n // Mark as being in the visibility window\n if (sprite) {\n sprite._inVisibilityWindow = true;\n }\n\n if (isDevelopment) {\n console.log(`Created full slide ${index} for ${image}`);\n }\n } else {\n // Create a placeholder for slides outside the visibility window\n const placeholderOptions = {\n width: sliderWidth,\n height: sliderHeight,\n color: 0x333333,\n showIndex: isDevelopment, // Show index in development mode\n index,\n trackWithResourceManager: true,\n resourceManager,\n renderer: app.renderer\n };\n\n // Import the createPlaceholderSprite function dynamically to avoid circular dependencies\n const { createPlaceholderSprite } = await import('../utils/placeholderUtils');\n\n // Create the placeholder sprite\n const placeholderSprite = createPlaceholderSprite(placeholderOptions);\n\n // Position the placeholder at the center\n placeholderSprite.x = app.screen.width / 2;\n placeholderSprite.y = app.screen.height / 2;\n\n // Set initial state - only show the first slide\n placeholderSprite.alpha = index === 0 ? 1 : 0;\n placeholderSprite.visible = index === 0;\n\n // Set scale similar to real sprites\n placeholderSprite.scale.set(1);\n placeholderSprite.baseScale = 1;\n\n // Mark as placeholder and outside visibility window\n placeholderSprite._isPlaceholder = true;\n placeholderSprite._placeholderIndex = index;\n placeholderSprite._inVisibilityWindow = false;\n\n // Add to container and store reference\n slidesContainer.addChild(placeholderSprite);\n pixi.slides.current.push(placeholderSprite);\n\n if (isDevelopment) {\n console.log(`Created placeholder for slide ${index} (outside visibility window)`);\n }\n }\n } catch (error) {\n if (isDevelopment) {\n console.error(`Error creating slide for ${image}:`, error);\n }\n }\n }\n\n setIsLoading(false);\n setLoadingProgress(100);\n } catch (error) {\n if (isDevelopment) {\n console.error(\"Error loading slide images:\", error);\n }\n setIsLoading(false);\n }\n };\n\n /**\n * Helper to create a slide sprite from a texture\n */\n const createSlideFromTexture = (\n texture: Texture,\n imagePath: string,\n index: number,\n slidesContainer: Container,\n app: any,\n sliderWidth: number,\n sliderHeight: number\n ): EnhancedSprite | null => {\n try {\n // Track texture with resource manager if available\n if (resourceManager) {\n resourceManager.trackTexture(imagePath, texture);\n }\n\n // Create the sprite\n const sprite = new Sprite(texture) as EnhancedSprite;\n sprite.anchor.set(0.5);\n sprite.x = app.screen.width / 2;\n sprite.y = app.screen.height / 2;\n\n // Set initial state - only show the first slide\n sprite.alpha = index === 0 ? 1 : 0;\n sprite.visible = index === 0;\n\n // Calculate and apply scale\n try {\n const { scale, baseScale } = calculateSpriteScale(\n texture.width,\n texture.height,\n sliderWidth,\n sliderHeight\n );\n\n sprite.scale.set(scale);\n sprite.baseScale = baseScale;\n } catch (scaleError) {\n if (isDevelopment) {\n console.warn(`Error calculating scale for slide ${index}:`, scaleError);\n }\n\n // Fallback scaling\n sprite.scale.set(1);\n sprite.baseScale = 1;\n }\n\n // Track the sprite with resource manager if available\n if (resourceManager) {\n resourceManager.trackDisplayObject(sprite);\n }\n\n // Add to container and store reference\n slidesContainer.addChild(sprite);\n pixi.slides.current.push(sprite);\n\n if (isDevelopment) {\n console.log(`Created slide ${index} for ${imagePath}`);\n }\n\n return sprite;\n } catch (error) {\n if (isDevelopment) {\n console.error(`Error in createSlideFromTexture for slide ${index}:`, error);\n }\n return null;\n }\n };\n\n /**\n * Utility function to load a slide at a specific index\n * @param index Index of the slide to load\n * @returns Promise that resolves when the slide is loaded\n */\n const loadSlideAtIndex = async (index: number): Promise<boolean> => {\n try {\n // Validate index\n if (index < 0 || index >= props.images.length) {\n if (isDevelopment) {\n console.warn(`Invalid slide index for loading: ${index}`);\n }\n return false;\n }\n\n // Get the current slide sprite\n const sprite = pixi.slides.current[index];\n\n // Skip if sprite doesn't exist or is not a placeholder or already loaded\n if (!sprite || !sprite._isPlaceholder || sprite._loadingState === 'loaded') {\n return true;\n }\n\n if (isDevelopment) {\n console.log(`Loading slide at index ${index}`);\n }\n\n // Update loading state\n sprite._loadingState = 'loading';\n\n // Get image path\n const imagePath = props.images[index];\n\n // Determine loading method based on atlas availability\n const useAtlas = atlasManager && props.slidesAtlas && areAssetsInAtlas() && isUseSlidesAtlasEnabled();\n let texture: Texture;\n\n if (useAtlas) {\n // Load from atlas\n const normalizedPath = normalizePath(imagePath);\n const atlasTexture = atlasManager.getFrameTexture(normalizedPath, props.slidesAtlas!);\n\n if (!atlasTexture) {\n throw new Error(`Frame ${normalizedPath} not found in atlas ${props.slidesAtlas}`);\n }\n\n texture = atlasTexture;\n } else {\n // Load from individual image\n texture = await Assets.load(imagePath);\n }\n\n // Store original texture for potential reuse\n sprite._originalTexture = texture;\n\n // Apply the texture\n sprite.texture = texture;\n\n // Calculate and apply scale\n if (sliderRef.current) {\n const sliderWidth = sliderRef.current.clientWidth;\n const sliderHeight = sliderRef.current.clientHeight;\n\n try {\n const { scale, baseScale } = calculateSpriteScale(\n texture.width,\n texture.height,\n sliderWidth,\n sliderHeight\n );\n\n sprite.scale.set(scale);\n sprite.baseScale = baseScale;\n } catch (scaleError) {\n if (isDevelopment) {\n console.warn(`Error calculating scale for loaded slide ${index}:`, scaleError);\n }\n\n // Fallback scaling\n sprite.scale.set(1);\n sprite.baseScale = 1;\n }\n }\n\n // Update flags\n sprite._isPlaceholder = false;\n sprite._inVisibilityWindow = true;\n sprite._loadingState = 'loaded';\n\n // Track the texture and update sprite in resource manager\n if (resourceManager) {\n resourceManager.trackTexture(imagePath, texture);\n resourceManager.trackDisplayObject(sprite);\n }\n\n if (isDevelopment) {\n console.log(`Successfully loaded slide at index ${index}`);\n }\n\n return true;\n } catch (error) {\n if (isDevelopment) {\n console.error(`Error loading slide at index ${index}:`, error);\n }\n // Update loading state to error\n const sprite = pixi.slides.current[index];\n if (sprite) {\n sprite._loadingState = 'error';\n }\n return false;\n }\n };\n\n /**\n * Enhanced transition function with better resource management and animation coordination\n */\n const transitionToSlide = useCallback((nextIndex: number): gsap.core.Timeline | null => {\n // Check if slider reference is available\n if (!sliderRef.current) {\n if (isDevelopment) {\n console.warn(\"Slider reference not available for transition\");\n }\n return null;\n }\n\n // Validate inputs\n if (!pixi.slides.current.length) {\n if (isDevelopment) {\n console.warn(\"No slides available for transition\");\n }\n return null;\n }\n\n if (nextIndex < 0 || nextIndex >= pixi.slides.current.length) {\n if (isDevelopment) {\n console.warn(`Invalid slide index: ${nextIndex}`);\n }\n return null;\n }\n\n try {\n // Cancel any active transition\n if (activeTransitionRef.current) {\n activeTransitionRef.current.kill();\n activeTransitionRef.current = null;\n }\n\n if (isDevelopment) {\n console.log(`Transitioning to slide ${nextIndex}`);\n }\n\n const currentIndex = pixi.currentIndex.current;\n const currentSlide = pixi.slides.current[currentIndex];\n const nextSlide = pixi.slides.current[nextIndex];\n\n // Update sliding window when changing slides\n if (slidingWindowManager) {\n // Update the central index in the sliding window\n slidingWindowManager.updateCurrentIndex(nextIndex);\n\n // Get the new visibility window\n const visibilityIndices = slidingWindowManager.getWindowIndices();\n\n if (isDevelopment) {\n console.log(`Sliding window updated. New visibility window: [${visibilityIndices.join(', ')}]`);\n }\n\n // Preload all slides in the visibility window\n // We do this asynchronously but don't wait for it\n Promise.all(\n visibilityIndices.map(async (index) => {\n // If the slide is a placeholder, load it\n const slideSprite = pixi.slides.current[index];\n if (slideSprite && slideSprite._isPlaceholder) {\n return loadSlideAtIndex(index);\n }\n return Promise.resolve(true);\n })\n ).then((results) => {\n if (isDevelopment) {\n const successCount = results.filter(result => result).length;\n console.log(`Preloaded ${successCount}/${visibilityIndices.length} slides in visibility window`);\n }\n });\n }\n\n // Check if the next slide is a placeholder, and if so, load it first\n const isNextSlideAPlaceholder = nextSlide._isPlaceholder === true;\n\n if (isNextSlideAPlaceholder) {\n if (isDevelopment) {\n console.log(`Next slide (${nextIndex}) is a placeholder. Loading it now...`);\n }\n\n // Return a promise that resolves with the timeline after loading\n return new Promise(async (resolve) => {\n try {\n // Load the slide\n const loadSuccess = await loadSlideAtIndex(nextIndex);\n\n if (!loadSuccess) {\n console.error(`Failed to load next slide at index ${nextIndex}`);\n resolve(null);\n return;\n }\n\n // Continue with the transition\n const timeline = performTransition(currentIndex, nextIndex);\n resolve(timeline);\n } catch (error) {\n console.error('Error loading next slide:', error);\n resolve(null);\n }\n }) as unknown as gsap.core.Timeline;\n }\n\n // If next slide is already loaded, directly perform the transition\n return performTransition(currentIndex, nextIndex);\n } catch (error) {\n if (isDevelopment) {\n console.error('Error during slide transition:', error);\n }\n return null;\n }\n }, [\n sliderRef,\n pixi.slides,\n pixi.textContainers,\n pixi.currentIndex,\n props.transitionScaleIntensity,\n resourceManager,\n onSlideChange,\n animationCoordinator,\n slidingWindowManager\n ]);\n\n /**\n * Helper function to perform the actual transition animation between slides\n */\n const performTransition = (currentIndex: number, nextIndex: number): gsap.core.Timeline | null => {\n try {\n const currentSlide = pixi.slides.current[currentIndex];\n const nextSlide = pixi.slides.current[nextIndex];\n\n // Handle text containers if available\n const textContainersAvailable =\n pixi.textContainers.current &&\n pixi.textContainers.current.length > currentIndex &&\n pixi.textContainers.current.length > nextIndex;\n\n const currentTextContainer = textContainersAvailable\n ? pixi.textContainers.current[currentIndex]\n : null;\n\n const nextTextContainer = textContainersAvailable\n ? pixi.textContainers.current[nextIndex]\n : null;\n\n // IMPORTANT: Make both slides visible during transition\n currentSlide.visible = true;\n nextSlide.visible = true;\n\n // Ensure next elements start invisible (alpha = 0)\n nextSlide.alpha = 0;\n if (nextTextContainer) {\n nextTextContainer.alpha = 0;\n nextTextContainer.visible = true; // Make next text visible before transition\n }\n\n // Calculate scale based on transition intensity\n const transitionScaleIntensity = props.transitionScaleIntensity ?? 30;\n const scaleMultiplier = 1 + transitionScaleIntensity / 100;\n\n // Create animations for slide transitions\n const slideOutAnimations: gsap.core.Tween[] = [];\n const slideInAnimations: gsap.core.Tween[] = [];\n const textOutAnimations: gsap.core.Tween[] = [];\n const textInAnimations: gsap.core.Tween[] = [];\n\n // Create slide out animations\n slideOutAnimations.push(\n gsap.to(currentSlide.scale, {\n x: currentSlide.baseScale! * scaleMultiplier,\n y: currentSlide.baseScale! * scaleMultiplier,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // Re-track the sprite after animation\n if (resourceManager) {\n resourceManager.trackDisplayObject(currentSlide);\n }\n }\n }),\n gsap.to(currentSlide, {\n alpha: 0,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // IMPORTANT: Hide previous slide after transition completes to save GPU\n currentSlide.visible = false;\n\n // Re-track the sprite after visibility change\n if (resourceManager) {\n resourceManager.trackDisplayObject(currentSlide);\n }\n }\n })\n );\n\n // Create slide in animations\n slideInAnimations.push(\n gsap.to(nextSlide.scale, {\n x: nextSlide.baseScale!,\n y: nextSlide.baseScale!,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // Re-track the sprite after animation\n if (resourceManager) {\n resourceManager.trackDisplayObject(nextSlide);\n }\n }\n }),\n gsap.to(nextSlide, {\n alpha: 1,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // Re-track the sprite after animation\n if (resourceManager) {\n resourceManager.trackDisplayObject(nextSlide);\n }\n }\n })\n );\n\n // Set initial scale for next slide\n nextSlide.scale.set(\n nextSlide.baseScale! * scaleMultiplier,\n nextSlide.baseScale! * scaleMultiplier\n );\n\n // Add text container animations if available\n if (currentTextContainer && nextTextContainer) {\n textOutAnimations.push(\n gsap.to(currentTextContainer, {\n alpha: 0,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // Hide previous text after transition\n currentTextContainer.visible = false;\n\n // Re-track the container after visibility change\n if (resourceManager) {\n resourceManager.trackDisplayObject(currentTextContainer);\n }\n }\n })\n );\n\n textInAnimations.push(\n gsap.to(nextTextContainer, {\n alpha: 1,\n duration: 1,\n ease: 'power2.out',\n onComplete: () => {\n // Re-track the container after animation\n if (resourceManager) {\n resourceManager.trackDisplayObject(nextTextContainer);\n }\n }\n })\n );\n }\n\n // Use the AnimationCoordinator to create coordinated animation groups\n const slideOutGroup = {\n id: `slide_out_${currentIndex}_${Date.now()}`,\n type: AnimationGroupType.SLIDE_TRANSITION,\n animations: slideOutAnimations\n };\n\n const slideInGroup = {\n id: `slide_in_${nextIndex}_${Date.now()}`,\n type: AnimationGroupType.SLIDE_TRANSITION,\n animations: slideInAnimations\n };\n\n // Create a master timeline for the entire transition\n const masterTimeline = gsap.timeline({\n onComplete: () => {\n // Update current index when transition completes\n pixi.currentIndex.current = nextIndex;\n activeTransitionRef.current = null;\n\n // Call the onSlideChange callback if provided\n if (onSlideChange) {\n onSlideChange(nextIndex);\n }\n\n // If using sliding window, check if any slides should be unloaded\n if (slidingWindowManager) {\n handleSlidingWindowUnload(nextIndex);\n }\n }\n });\n\n // Add slide animations to the master timeline\n const slideOutTimeline = animationCoordinator.createAnimationGroup(slideOutGroup);\n const slideInTimeline = animationCoordinator.createAnimationGroup(slideInGroup);\n\n masterTimeline.add(slideOutTimeline, 0);\n masterTimeline.add(slideInTimeline, 0);\n\n // Add text animations if available\n if (textOutAnimations.length > 0 && textInAnimations.length > 0) {\n const textOutGroup = {\n id: `text_out_${currentIndex}_${Date.now()}`,\n type: AnimationGroupType.TEXT_ANIMATION,\n animations: textOutAnimations\n };\n\n const textInGroup = {\n id: `text_in_${nextIndex}_${Date.now()}`,\n type: AnimationGroupType.TEXT_ANIMATION,\n animations: textInAnimations\n };\n\n const textOutTimeline = animationCoordinator.createAnimationGroup(textOutGroup);\n const textInTimeline = animationCoordinator.createAnimationGroup(textInGroup);\n\n masterTimeline.add(textOutTimeline, 0);\n masterTimeline.add(textInTimeline, 0);\n }\n\n // Store the master timeline\n activeTransitionRef.current = masterTimeline;\n\n // Track the master timeline with resourceManager\n if (resourceManager) {\n resourceManager.trackAnimation(masterTimeline);\n }\n\n return masterTimeline;\n } catch (error) {\n if (isDevelopment) {\n console.error('Error during slide transition animation:', error);\n }\n return null;\n }\n };\n\n /**\n * Helper function to handle unloading slides that are far outside the visibility window\n */\n const handleSlidingWindowUnload = (currentIndex: number) => {\n if (!slidingWindowManager) return;\n\n // Get current visibility window\n const visibilityIndices = slidingWindowManager.getWindowIndices();\n\n // Check all loaded slides and unload those that are far outside the visibility window\n pixi.slides.current.forEach((sprite, index) => {\n // Skip if sprite is already a placeholder or uninitialized\n if (sprite._isPlaceholder || sprite._loadingState === 'uninitialized') {\n return;\n }\n\n // If this slide is outside the visibility window and not the current slide\n if (!visibilityIndices.includes(index) && index !== currentIndex) {\n // How far outside the window is this slide?\n const distanceFromWindow = Math.min(\n ...visibilityIndices.map(visIndex => Math.abs(index - visIndex))\n );\n\n // Only unload if it's far enough away (e.g., more than 2 slides away from any visible slide)\n const unloadThreshold = slidingWindowManager.getWindowSize() + 1;\n if (distanceFromWindow > unloadThreshold) {\n // Don't unload slides that are very close to the current index\n const distanceFromCurrent = Math.abs(index - currentIndex);\n if (distanceFromCurrent <= unloadThreshold) {\n return;\n }\n\n if (isDevelopment) {\n console.log(`Unloading slide ${index} (distance from window: ${distanceFromWindow})`);\n }\n\n // Import placeholder utilities\n import('../utils/placeholderUtils').then(({ createPlaceholderSprite }) => {\n // Create placeholder to replace the loaded sprite\n if (sliderRef.current && pixi.app.current) {\n const sliderWidth = sliderRef.current.clientWidth;\n const sliderHeight = sliderRef.current.clientHeight;\n\n const placeholderOptions = {\n width: sliderWidth,\n height: sliderHeight,\n color: 0x333333,\n showIndex: isDevelopment,\n index,\n trackWithResourceManager: true,\n resourceManager,\n renderer: pixi.app.current.renderer\n };\n\n // Create a placeholder\n const placeholderSprite = createPlaceholderSprite(placeholderOptions);\n\n // Copy position and other properties\n placeholderSprite.x = sprite.x;\n placeholderSprite.y = sprite.y;\n placeholderSprite.alpha = sprite.alpha;\n placeholderSprite.visible = sprite.visible;\n placeholderSprite.baseScale = sprite.baseScale;\n placeholderSprite.scale.set(sprite.scale.x, sprite.scale.y);\n\n // Mark as outside visibility window\n placeholderSprite._inVisibilityWindow = false;\n\n // Store original texture for potential reuse\n placeholderSprite._originalTexture = sprite.texture;\n\n // Replace in the container\n if (sprite.parent) {\n const parent = sprite.parent;\n const spriteIndex = parent.getChildIndex(sprite);\n parent.addChildAt(placeholderSprite, spriteIndex);\n parent.removeChild(sprite);\n }\n\n // Replace in the slides array\n pixi.slides.current[index] = placeholderSprite;\n\n // Properly dispose the sprite (but keep the texture)\n if (resourceManager) {\n // First, remove from ResourceManager's tracking\n resourceManager.trackDisplayObject(sprite);\n\n // Then destroy the sprite, but keep the texture\n