UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

662 lines (657 loc) 25.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); var pixi_js = require('pixi.js'); var calculateSpriteScale = require('../utils/calculateSpriteScale.cjs'); var gsap = require('gsap'); var AnimationCoordinator = require('../managers/AnimationCoordinator.cjs'); const isDevelopment = false; const useSlides = ({ sliderRef, pixi, props, resourceManager, atlasManager, onSlideChange, slidingWindowManager }) => { console.log("useSlides received useSlidesAtlas:", props.useSlidesAtlas); console.log("useSlides received props:", props); const [isLoading, setIsLoading] = react.useState(false); const [loadingProgress, setLoadingProgress] = react.useState(0); const activeTransitionRef = react.useRef(null); const animationCoordinator = AnimationCoordinator.AnimationCoordinator.getInstance(); props.slidesBasePath || "/images/"; const normalizePath = (imagePath) => { if (imagePath.startsWith("/")) { return imagePath.substring(1); } return imagePath; }; const isUseSlidesAtlasEnabled = () => { if (props.useSlidesAtlas === true) return true; if (typeof props.useSlidesAtlas === "string" && props.useSlidesAtlas === "true") return true; if (typeof props.useSlidesAtlas === "number" && props.useSlidesAtlas === 1) return true; if (typeof props.useSlidesAtlas === "string" && props.useSlidesAtlas === "1") return true; return false; }; const areAssetsInAtlas = react.useCallback(() => { if (!atlasManager || !props.slidesAtlas) { return false; } const useSlidesAtlasEnabled = isUseSlidesAtlasEnabled(); if (!useSlidesAtlasEnabled) { return false; } const result = props.images.every((imagePath) => { const normalizedPath = normalizePath(imagePath); const atlasId = atlasManager.hasFrame(normalizedPath); return !!atlasId; }); return result; }, [atlasManager, props.images, props.slidesAtlas, props.useSlidesAtlas]); react.useEffect(() => { if (!pixi.app.current || !pixi.app.current.stage) { return; } if (!props.images.length) { return; } if (!sliderRef.current) { return; } let slidesContainer; try { const app = pixi.app.current; if (app.stage.children.length > 0 && app.stage.children[0] instanceof pixi_js.Container) { slidesContainer = app.stage.children[0]; } else { slidesContainer = new pixi_js.Container(); slidesContainer.label = "slidesContainer"; app.stage.addChild(slidesContainer); if (resourceManager) { resourceManager.trackDisplayObject(slidesContainer); } } pixi.slides.current.forEach((sprite) => { if (sprite && sprite.parent) { try { sprite.parent.removeChild(sprite); } catch (error) { if (isDevelopment) ; } } }); pixi.slides.current = []; const useSlidesAtlasEnabled = isUseSlidesAtlasEnabled(); const useAtlas = atlasManager && props.slidesAtlas && areAssetsInAtlas() && useSlidesAtlasEnabled; if (isDevelopment) ; if (useAtlas) { loadSlidesFromAtlas(slidesContainer); } else { loadSlidesFromIndividualImages(slidesContainer); } } catch (error) { setIsLoading(false); } }, [pixi.app.current, props.images, resourceManager, sliderRef, atlasManager, props.slidesAtlas, props.useSlidesAtlas]); const loadSlidesFromAtlas = async (slidesContainer) => { if (!pixi.app.current || !sliderRef.current || !atlasManager) return; try { setIsLoading(true); setLoadingProgress(0); if (isDevelopment) ; const app = pixi.app.current; const sliderWidth = sliderRef.current.clientWidth; const sliderHeight = sliderRef.current.clientHeight; const totalImages = props.images.length; let loadedCount = 0; const visibilityWindowIndices = slidingWindowManager ? slidingWindowManager.getWindowIndices() : []; if (isDevelopment && slidingWindowManager) ; for (const [index, imagePath] of props.images.entries()) { try { const isInVisibilityWindow = !slidingWindowManager || visibilityWindowIndices.includes(index); if (isDevelopment && slidingWindowManager) ; if (isInVisibilityWindow) { const normalizedPath = normalizePath(imagePath); if (isDevelopment) ; const texture = atlasManager.getFrameTexture(normalizedPath, props.slidesAtlas); if (!texture) { throw new Error(`Frame ${normalizedPath} not found in atlas ${props.slidesAtlas}`); } if (resourceManager) { resourceManager.trackTexture(imagePath, texture); } const sprite = new pixi_js.Sprite(texture); sprite.anchor.set(0.5); sprite.x = app.screen.width / 2; sprite.y = app.screen.height / 2; sprite.alpha = index === 0 ? 1 : 0; sprite.visible = index === 0; try { const { scale, baseScale } = calculateSpriteScale.calculateSpriteScale( texture.width, texture.height, sliderWidth, sliderHeight ); sprite.scale.set(scale); sprite.baseScale = baseScale; } catch (scaleError) { if (isDevelopment) ; sprite.scale.set(1); sprite.baseScale = 1; } sprite._inVisibilityWindow = true; if (resourceManager) { resourceManager.trackDisplayObject(sprite); } slidesContainer.addChild(sprite); pixi.slides.current.push(sprite); if (isDevelopment) ; } else { const placeholderOptions = { width: sliderWidth, height: sliderHeight, color: 3355443, showIndex: isDevelopment, // Show index in development mode index, trackWithResourceManager: true, resourceManager, renderer: app.renderer }; const { createPlaceholderSprite } = await Promise.resolve().then(function () { return require('../utils/placeholderUtils.cjs'); }); const placeholderSprite = createPlaceholderSprite(placeholderOptions); placeholderSprite.x = app.screen.width / 2; placeholderSprite.y = app.screen.height / 2; placeholderSprite.alpha = index === 0 ? 1 : 0; placeholderSprite.visible = index === 0; placeholderSprite.scale.set(1); placeholderSprite.baseScale = 1; placeholderSprite._isPlaceholder = true; placeholderSprite._placeholderIndex = index; placeholderSprite._inVisibilityWindow = false; slidesContainer.addChild(placeholderSprite); pixi.slides.current.push(placeholderSprite); if (isDevelopment) ; } loadedCount++; const progress = loadedCount / totalImages * 100; setLoadingProgress(progress); } catch (error) { if (isDevelopment) ; const texture = await pixi_js.Assets.load(imagePath); createSlideFromTexture(texture, imagePath, index, slidesContainer, app, sliderWidth, sliderHeight); loadedCount++; setLoadingProgress(loadedCount / totalImages * 100); } } setIsLoading(false); setLoadingProgress(100); if (isDevelopment) ; } catch (error) { loadSlidesFromIndividualImages(slidesContainer); } }; const loadSlidesFromIndividualImages = async (slidesContainer) => { if (!pixi.app.current || !sliderRef.current) return; try { setIsLoading(true); setLoadingProgress(0); if (isDevelopment) ; const app = pixi.app.current; const sliderWidth = sliderRef.current.clientWidth; const sliderHeight = sliderRef.current.clientHeight; if (isDevelopment) ; const visibilityWindowIndices = slidingWindowManager ? slidingWindowManager.getWindowIndices() : []; if (isDevelopment && slidingWindowManager) ; const imagesToLoad = props.images.filter((_, index) => !slidingWindowManager || visibilityWindowIndices.includes(index)).filter((image) => !pixi_js.Assets.cache.has(image)); if (isDevelopment) ; if (imagesToLoad.length > 0) { pixi_js.Assets.addBundle("slider-images", imagesToLoad.reduce((acc, image, index) => { acc[`slide-${index}`] = image; return acc; }, {})); await pixi_js.Assets.loadBundle("slider-images", (progress) => { setLoadingProgress(progress * 100); }); } for (const [index, image] of props.images.entries()) { try { const isInVisibilityWindow = !slidingWindowManager || visibilityWindowIndices.includes(index); if (isDevelopment && slidingWindowManager) ; if (isInVisibilityWindow) { const texture = pixi_js.Assets.get(image); const sprite = createSlideFromTexture(texture, image, index, slidesContainer, app, sliderWidth, sliderHeight); if (sprite) { sprite._inVisibilityWindow = true; } if (isDevelopment) ; } else { const placeholderOptions = { width: sliderWidth, height: sliderHeight, color: 3355443, showIndex: isDevelopment, // Show index in development mode index, trackWithResourceManager: true, resourceManager, renderer: app.renderer }; const { createPlaceholderSprite } = await Promise.resolve().then(function () { return require('../utils/placeholderUtils.cjs'); }); const placeholderSprite = createPlaceholderSprite(placeholderOptions); placeholderSprite.x = app.screen.width / 2; placeholderSprite.y = app.screen.height / 2; placeholderSprite.alpha = index === 0 ? 1 : 0; placeholderSprite.visible = index === 0; placeholderSprite.scale.set(1); placeholderSprite.baseScale = 1; placeholderSprite._isPlaceholder = true; placeholderSprite._placeholderIndex = index; placeholderSprite._inVisibilityWindow = false; slidesContainer.addChild(placeholderSprite); pixi.slides.current.push(placeholderSprite); if (isDevelopment) ; } } catch (error) { if (isDevelopment) ; } } setIsLoading(false); setLoadingProgress(100); } catch (error) { setIsLoading(false); } }; const createSlideFromTexture = (texture, imagePath, index, slidesContainer, app, sliderWidth, sliderHeight) => { try { if (resourceManager) { resourceManager.trackTexture(imagePath, texture); } const sprite = new pixi_js.Sprite(texture); sprite.anchor.set(0.5); sprite.x = app.screen.width / 2; sprite.y = app.screen.height / 2; sprite.alpha = index === 0 ? 1 : 0; sprite.visible = index === 0; try { const { scale, baseScale } = calculateSpriteScale.calculateSpriteScale( texture.width, texture.height, sliderWidth, sliderHeight ); sprite.scale.set(scale); sprite.baseScale = baseScale; } catch (scaleError) { if (isDevelopment) ; sprite.scale.set(1); sprite.baseScale = 1; } if (resourceManager) { resourceManager.trackDisplayObject(sprite); } slidesContainer.addChild(sprite); pixi.slides.current.push(sprite); if (isDevelopment) ; return sprite; } catch (error) { return null; } }; const loadSlideAtIndex = async (index) => { try { if (index < 0 || index >= props.images.length) { if (isDevelopment) ; return false; } const sprite = pixi.slides.current[index]; if (!sprite || !sprite._isPlaceholder || sprite._loadingState === "loaded") { return true; } if (isDevelopment) ; sprite._loadingState = "loading"; const imagePath = props.images[index]; const useAtlas = atlasManager && props.slidesAtlas && areAssetsInAtlas() && isUseSlidesAtlasEnabled(); let texture; if (useAtlas) { const normalizedPath = normalizePath(imagePath); const atlasTexture = atlasManager.getFrameTexture(normalizedPath, props.slidesAtlas); if (!atlasTexture) { throw new Error(`Frame ${normalizedPath} not found in atlas ${props.slidesAtlas}`); } texture = atlasTexture; } else { texture = await pixi_js.Assets.load(imagePath); } sprite._originalTexture = texture; sprite.texture = texture; if (sliderRef.current) { const sliderWidth = sliderRef.current.clientWidth; const sliderHeight = sliderRef.current.clientHeight; try { const { scale, baseScale } = calculateSpriteScale.calculateSpriteScale( texture.width, texture.height, sliderWidth, sliderHeight ); sprite.scale.set(scale); sprite.baseScale = baseScale; } catch (scaleError) { if (isDevelopment) ; sprite.scale.set(1); sprite.baseScale = 1; } } sprite._isPlaceholder = false; sprite._inVisibilityWindow = true; sprite._loadingState = "loaded"; if (resourceManager) { resourceManager.trackTexture(imagePath, texture); resourceManager.trackDisplayObject(sprite); } if (isDevelopment) ; return true; } catch (error) { const sprite = pixi.slides.current[index]; if (sprite) { sprite._loadingState = "error"; } return false; } }; const transitionToSlide = react.useCallback((nextIndex) => { if (!sliderRef.current) { return null; } if (!pixi.slides.current.length) { return null; } if (nextIndex < 0 || nextIndex >= pixi.slides.current.length) { return null; } try { if (activeTransitionRef.current) { activeTransitionRef.current.kill(); activeTransitionRef.current = null; } if (isDevelopment) ; const currentIndex = pixi.currentIndex.current; const currentSlide = pixi.slides.current[currentIndex]; const nextSlide2 = pixi.slides.current[nextIndex]; if (slidingWindowManager) { slidingWindowManager.updateCurrentIndex(nextIndex); const visibilityIndices = slidingWindowManager.getWindowIndices(); if (isDevelopment) ; Promise.all( visibilityIndices.map(async (index) => { const slideSprite = pixi.slides.current[index]; if (slideSprite && slideSprite._isPlaceholder) { return loadSlideAtIndex(index); } return Promise.resolve(true); }) ).then((results) => { if (isDevelopment) ; }); } const isNextSlideAPlaceholder = nextSlide2._isPlaceholder === true; if (isNextSlideAPlaceholder) { if (isDevelopment) ; return new Promise(async (resolve) => { try { const loadSuccess = await loadSlideAtIndex(nextIndex); if (!loadSuccess) { console.error(`Failed to load next slide at index ${nextIndex}`); resolve(null); return; } const timeline = performTransition(currentIndex, nextIndex); resolve(timeline); } catch (error) { console.error("Error loading next slide:", error); resolve(null); } }); } return performTransition(currentIndex, nextIndex); } catch (error) { return null; } }, [ sliderRef, pixi.slides, pixi.textContainers, pixi.currentIndex, props.transitionScaleIntensity, resourceManager, onSlideChange, animationCoordinator, slidingWindowManager ]); const performTransition = (currentIndex, nextIndex) => { try { const currentSlide = pixi.slides.current[currentIndex]; const nextSlide2 = pixi.slides.current[nextIndex]; const textContainersAvailable = pixi.textContainers.current && pixi.textContainers.current.length > currentIndex && pixi.textContainers.current.length > nextIndex; const currentTextContainer = textContainersAvailable ? pixi.textContainers.current[currentIndex] : null; const nextTextContainer = textContainersAvailable ? pixi.textContainers.current[nextIndex] : null; currentSlide.visible = true; nextSlide2.visible = true; nextSlide2.alpha = 0; if (nextTextContainer) { nextTextContainer.alpha = 0; nextTextContainer.visible = true; } const transitionScaleIntensity = props.transitionScaleIntensity ?? 30; const scaleMultiplier = 1 + transitionScaleIntensity / 100; const slideOutAnimations = []; const slideInAnimations = []; const textOutAnimations = []; const textInAnimations = []; slideOutAnimations.push( gsap.gsap.to(currentSlide.scale, { x: currentSlide.baseScale * scaleMultiplier, y: currentSlide.baseScale * scaleMultiplier, duration: 1, ease: "power2.out", onComplete: () => { if (resourceManager) { resourceManager.trackDisplayObject(currentSlide); } } }), gsap.gsap.to(currentSlide, { alpha: 0, duration: 1, ease: "power2.out", onComplete: () => { currentSlide.visible = false; if (resourceManager) { resourceManager.trackDisplayObject(currentSlide); } } }) ); slideInAnimations.push( gsap.gsap.to(nextSlide2.scale, { x: nextSlide2.baseScale, y: nextSlide2.baseScale, duration: 1, ease: "power2.out", onComplete: () => { if (resourceManager) { resourceManager.trackDisplayObject(nextSlide2); } } }), gsap.gsap.to(nextSlide2, { alpha: 1, duration: 1, ease: "power2.out", onComplete: () => { if (resourceManager) { resourceManager.trackDisplayObject(nextSlide2); } } }) ); nextSlide2.scale.set( nextSlide2.baseScale * scaleMultiplier, nextSlide2.baseScale * scaleMultiplier ); if (currentTextContainer && nextTextContainer) { textOutAnimations.push( gsap.gsap.to(currentTextContainer, { alpha: 0, duration: 1, ease: "power2.out", onComplete: () => { currentTextContainer.visible = false; if (resourceManager) { resourceManager.trackDisplayObject(currentTextContainer); } } }) ); textInAnimations.push( gsap.gsap.to(nextTextContainer, { alpha: 1, duration: 1, ease: "power2.out", onComplete: () => { if (resourceManager) { resourceManager.trackDisplayObject(nextTextContainer); } } }) ); } const slideOutGroup = { id: `slide_out_${currentIndex}_${Date.now()}`, type: AnimationCoordinator.AnimationGroupType.SLIDE_TRANSITION, animations: slideOutAnimations }; const slideInGroup = { id: `slide_in_${nextIndex}_${Date.now()}`, type: AnimationCoordinator.AnimationGroupType.SLIDE_TRANSITION, animations: slideInAnimations }; const masterTimeline = gsap.gsap.timeline({ onComplete: () => { pixi.currentIndex.current = nextIndex; activeTransitionRef.current = null; if (onSlideChange) { onSlideChange(nextIndex); } if (slidingWindowManager) { handleSlidingWindowUnload(nextIndex); } } }); const slideOutTimeline = animationCoordinator.createAnimationGroup(slideOutGroup); const slideInTimeline = animationCoordinator.createAnimationGroup(slideInGroup); masterTimeline.add(slideOutTimeline, 0); masterTimeline.add(slideInTimeline, 0); if (textOutAnimations.length > 0 && textInAnimations.length > 0) { const textOutGroup = { id: `text_out_${currentIndex}_${Date.now()}`, type: AnimationCoordinator.AnimationGroupType.TEXT_ANIMATION, animations: textOutAnimations }; const textInGroup = { id: `text_in_${nextIndex}_${Date.now()}`, type: AnimationCoordinator.AnimationGroupType.TEXT_ANIMATION, animations: textInAnimations }; const textOutTimeline = animationCoordinator.createAnimationGroup(textOutGroup); const textInTimeline = animationCoordinator.createAnimationGroup(textInGroup); masterTimeline.add(textOutTimeline, 0); masterTimeline.add(textInTimeline, 0); } activeTransitionRef.current = masterTimeline; if (resourceManager) { resourceManager.trackAnimation(masterTimeline); } return masterTimeline; } catch (error) { return null; } }; const handleSlidingWindowUnload = (currentIndex) => { if (!slidingWindowManager) return; const visibilityIndices = slidingWindowManager.getWindowIndices(); pixi.slides.current.forEach((sprite, index) => { if (sprite._isPlaceholder || sprite._loadingState === "uninitialized") { return; } if (!visibilityIndices.includes(index) && index !== currentIndex) { const distanceFromWindow = Math.min( ...visibilityIndices.map((visIndex) => Math.abs(index - visIndex)) ); const unloadThreshold = slidingWindowManager.getWindowSize() + 1; if (distanceFromWindow > unloadThreshold) { const distanceFromCurrent = Math.abs(index - currentIndex); if (distanceFromCurrent <= unloadThreshold) { return; } Promise.resolve().then(function () { return require('../utils/placeholderUtils.cjs'); }).then(({ createPlaceholderSprite }) => { if (sliderRef.current && pixi.app.current) { const sliderWidth = sliderRef.current.clientWidth; const sliderHeight = sliderRef.current.clientHeight; const placeholderOptions = { width: sliderWidth, height: sliderHeight, color: 3355443, showIndex: isDevelopment, index, trackWithResourceManager: true, resourceManager, renderer: pixi.app.current.renderer }; const placeholderSprite = createPlaceholderSprite(placeholderOptions); placeholderSprite.x = sprite.x; placeholderSprite.y = sprite.y; placeholderSprite.alpha = sprite.alpha; placeholderSprite.visible = sprite.visible; placeholderSprite.baseScale = sprite.baseScale; placeholderSprite.scale.set(sprite.scale.x, sprite.scale.y); placeholderSprite._inVisibilityWindow = false; placeholderSprite._originalTexture = sprite.texture; if (sprite.parent) { const parent = sprite.parent; const spriteIndex = parent.getChildIndex(sprite); parent.addChildAt(placeholderSprite, spriteIndex); parent.removeChild(sprite); } pixi.slides.current[index] = placeholderSprite; if (resourceManager) { resourceManager.trackDisplayObject(sprite); sprite.destroy({ children: true, texture: false }); } else { sprite.destroy({ children: true, texture: false }); } } }); } } }); }; const nextSlide = react.useCallback((nextIndex) => { const tl = transitionToSlide(nextIndex); if (tl && onSlideChange) { onSlideChange(nextIndex); } }, [transitionToSlide, onSlideChange]); const prevSlide = react.useCallback((prevIndex) => { const tl = transitionToSlide(prevIndex); if (tl && onSlideChange) { onSlideChange(prevIndex); } }, [transitionToSlide, onSlideChange]); return { transitionToSlide, nextSlide, prevSlide, isLoading, loadingProgress }; }; exports.default = useSlides; exports.useSlides = useSlides; //# sourceMappingURL=useSlides.cjs.map