UNPKG

kinetic-slider

Version:

A WebGL-powered kinetic slider component using PIXI.js

824 lines (821 loc) 26.6 kB
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 './managers/ShaderResourceManager.js'; 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'; const FILTER_COORDINATION_EVENT = "kinetic-slider:filter-update"; 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 }) => { console.log("KineticSlider received props:", { useSlidesAtlas, useEffectsAtlas, slidesAtlas, effectsAtlas }); 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); const scheduler = RenderScheduler.getInstance(); const resourceManagerRef = useRef(null); const slidingWindowManagerRef = useRef(null); const animationCoordinator = AnimationCoordinator.getInstance(); useEffect(() => { if (typeof window === "undefined") return; scheduler.configureThrottling({ targetFps: 60, strategy: ThrottleStrategy.PRIORITY }); return () => { scheduler.clearQueue(); }; }, [scheduler]); const atlasManagerRef = useRef(null); 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); useEffect(() => { setIsClient(true); }, []); useEffect(() => { const componentId = `kinetic-slider-${Math.random().toString(36).substring(2, 9)}`; resourceManagerRef.current = new ResourceManager(componentId, { enableMetrics: true, enableShaderPooling: true, logLevel: "warn" }); FilterFactory.initialize({ enableShaderPooling: true, enableDebug: false, lazyLoadConfig: { unloadTimeoutMs: 12e4, // 2 minutes maxCachedModules: 20, enablePrefetching: true, retryFailedLoads: true, maxRetries: 3 } }); atlasManagerRef.current = new AtlasManager({ debug: true, preferAtlas: true, cacheFrameTextures: true, basePath: "/atlas" }, resourceManagerRef.current); if (resourceManagerRef.current) { animationCoordinator.setResourceManager(resourceManagerRef.current); } slidingWindowManagerRef.current = new SlidingWindowManager({ totalSlides: images.length, initialIndex: 0, windowSize: 2, // Default: current slide ±2 debug: false }, resourceManagerRef.current); return () => { if (resourceManagerRef.current) { console.log("Component unmounting - disposing all resources"); resourceManagerRef.current.markUnmounting(); resourceManagerRef.current.dispose(); resourceManagerRef.current = null; } if (atlasManagerRef.current) { atlasManagerRef.current.dispose(); atlasManagerRef.current = null; } slidingWindowManagerRef.current = null; }; }, [images.length, animationCoordinator]); const pixiRefs = { app: appRef, slides: slidesRef, textContainers: textContainersRef, backgroundDisplacementSprite: backgroundDisplacementSpriteRef, cursorDisplacementSprite: cursorDisplacementSpriteRef, bgDispFilter: bgDispFilterRef, cursorDispFilter: cursorDispFilterRef, currentIndex: currentIndexRef }; 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 }; const hookParams = { sliderRef, pixi: pixiRefs, props: hookProps, resourceManager: resourceManagerRef.current, atlasManager: atlasManagerRef.current, slidingWindowManager: slidingWindowManagerRef.current }; const { showDisplacementEffects, hideDisplacementEffects } = useDisplacementEffects({ sliderRef, bgDispFilterRef, cursorDispFilterRef, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, appRef, backgroundDisplacementSpriteLocation, cursorDisplacementSpriteLocation, cursorImgEffect, cursorScaleIntensity, cursorDisplacementSizing, cursorDisplacementWidth, cursorDisplacementHeight, resourceManager: resourceManagerRef.current, atlasManager: atlasManagerRef.current || void 0, effectsAtlas, useEffectsAtlas }); const { updateFilterIntensities, resetAllFilters, activateFilterEffects, isInitialized: filtersInitialized, isActive: filtersActive, setFiltersActive } = useFilters(hookParams); useEffect(() => { if (!isAppReady || !assetsLoaded) { console.log("Skipping filter coordination - not ready", { filtersInitialized, isAppReady, assetsLoaded }); return; } if (!filtersInitialized && typeof activateFilterEffects === "function") { console.log("Attempting to initialize filters during coordination"); activateFilterEffects(); } console.log(`Filter coordination - isInteracting: ${isInteracting}, filtersActive: ${filtersActive}`); if (isInteracting) { console.log("Interaction started - activating effects"); showDisplacementEffects(); if (typeof activateFilterEffects === "function") { console.log("Using activateFilterEffects"); activateFilterEffects(); } else { console.log("Using updateFilterIntensities"); updateFilterIntensities(true, true); } } else { console.log("Interaction ended - deactivating effects"); hideDisplacementEffects(); updateFilterIntensities(false); } }, [ isInteracting, isAppReady, assetsLoaded, filtersInitialized, filtersActive, showDisplacementEffects, hideDisplacementEffects, updateFilterIntensities, activateFilterEffects ]); useEffect(() => { if (!isAppReady || !assetsLoaded) { resetAllFilters(); } return () => { resetAllFilters(); }; }, [isAppReady, assetsLoaded, resetAllFilters]); useEffect(() => { if (typeof window === "undefined" || !isClient) return; const loadAssets = async () => { try { console.log("Preloading assets and fonts..."); if (atlasManagerRef.current) { if (slidesAtlas) { await atlasManagerRef.current.loadAtlas( slidesAtlas, `/atlas/${slidesAtlas}.json`, `/atlas/${slidesAtlas}.png` ); } if (effectsAtlas) { await atlasManagerRef.current.loadAtlas( effectsAtlas, `/atlas/${effectsAtlas}.json`, `/atlas/${effectsAtlas}.png` ); } } 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); setAssetsLoaded(true); } }; loadAssets(); }, [ isClient, images, backgroundDisplacementSpriteLocation, cursorDisplacementSpriteLocation, textTitleFontFamily, textSubTitleFontFamily, slidesAtlas, effectsAtlas ]); useEffect(() => { if (typeof window === "undefined" || !sliderRef.current || appRef.current || !assetsLoaded) return; const initPixi = async () => { try { console.log("Loading PixiJS dependencies..."); const { gsap, pixi, pixiPlugin } = await loadKineticSliderDependencies(); if (typeof window !== "undefined" && pixiPlugin) { gsap.registerPlugin(pixiPlugin); if (pixiPlugin.registerPIXI) { pixiPlugin.registerPIXI(pixi); } } console.log("Creating Pixi.js application..."); const app = new Application(); await app.init({ width: sliderRef.current?.clientWidth || 800, height: sliderRef.current?.clientHeight || 600, backgroundAlpha: 0, resizeTo: sliderRef.current || void 0 }); if (resourceManagerRef.current) { resourceManagerRef.current.trackPixiApp(app); } if (sliderRef.current) { sliderRef.current.appendChild(app.canvas); } appRef.current = app; const stage = new Container(); app.stage.addChild(stage); if (resourceManagerRef.current) { resourceManagerRef.current.trackDisplayObject(stage); } setIsAppReady(true); console.log("Pixi.js application initialized"); } catch (error) { console.error("Failed to initialize Pixi.js application:", error); } }; initPixi(); return () => { if (appRef.current) { if (sliderRef.current) { const canvas = sliderRef.current.querySelector("canvas"); if (canvas) { sliderRef.current.removeChild(canvas); } } appRef.current = null; setIsAppReady(false); } }; }, [sliderRef.current, assetsLoaded]); useEffect(() => { if (typeof window === "undefined" || !isAppReady) return; if (!hookProps.imageFilters && !hookProps.textFilters) return; 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); }; const imageFilterTypes = getFilterTypes(hookProps.imageFilters); const textFilterTypes = getFilterTypes(hookProps.textFilters); const allFilterTypes = [.../* @__PURE__ */ new Set([...imageFilterTypes, ...textFilterTypes])]; if (allFilterTypes.length > 0) { console.log(`Prefetching ${allFilterTypes.length} filter types:`, allFilterTypes); FilterFactory.prefetchFilterModules(allFilterTypes, "high"); } }, [isAppReady, hookProps.imageFilters, hookProps.textFilters]); const handleSlideChange = useCallback((newIndex) => { currentIndexRef.current = newIndex; setCurrentSlideIndex(newIndex); if (filtersInitialized) { if (isInteracting) { console.log("Cursor is within canvas - applying filters immediately with critical priority"); scheduler.scheduleTypedUpdate( "slider", UpdateType.DISPLACEMENT_EFFECT, () => { showDisplacementEffects(); }, "critical" ); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { currentIndexRef.current = newIndex; if (typeof activateFilterEffects === "function") { activateFilterEffects(); setFiltersActive(true); } }, "critical" ); } else { activateFilterEffects(); const transitionDuration = 1e3; setTimeout(() => { if (!isInteracting) { updateFilterIntensities(false); hideDisplacementEffects(); } }, transitionDuration); } } }, [ filtersInitialized, isInteracting, activateFilterEffects, updateFilterIntensities, hideDisplacementEffects, showDisplacementEffects, scheduler, setFiltersActive ]); const { nextSlide, prevSlide } = useSlides({ ...hookParams, onSlideChange: handleSlideChange }); useTextContainers({ sliderRef, appRef, slidesRef, textContainersRef, currentIndex: currentIndexRef, buttonMode, texts, textTitleColor, textTitleSize, mobileTextTitleSize, textTitleLetterspacing, textTitleFontFamily, textSubTitleColor, textSubTitleSize, mobileTextSubTitleSize, textSubTitleLetterspacing, textSubTitleOffsetTop, mobileTextSubTitleOffsetTop, textSubTitleFontFamily, resourceManager: resourceManagerRef.current }); useMouseTracking({ ...hookParams, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef, cursorImgEffect, cursorMomentum }); useIdleTimer({ sliderRef, cursorActive: cursorActiveRef, bgDispFilterRef, cursorDispFilterRef, cursorImgEffect, defaultBgFilterScale: 20, defaultCursorFilterScale: 10, resourceManager: resourceManagerRef.current }); const handleNext = useCallback(() => { if (!appRef.current || !isAppReady || slidesRef.current.length === 0) return; const nextIndex = (currentSlideIndex + 1) % slidesRef.current.length; scheduler.scheduleTypedUpdate( "slider", UpdateType.SLIDE_TRANSITION, () => { nextSlide(nextIndex); currentIndexRef.current = nextIndex; setCurrentSlideIndex(nextIndex); setTimeout(() => { console.log("Scheduling effects after slide change (next)"); scheduler.scheduleTypedUpdate( "slider", UpdateType.DISPLACEMENT_EFFECT, () => { showDisplacementEffects(); }, // Use critical priority if cursor is within the canvas isInteracting ? "critical" : void 0 ); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { currentIndexRef.current = nextIndex; if (typeof activateFilterEffects === "function") { activateFilterEffects(); if (isInteracting) { setFiltersActive(true); } } }, // Use critical priority if cursor is within the canvas isInteracting ? "critical" : void 0 ); }, 100); } ); }, [ appRef, isAppReady, slidesRef, currentSlideIndex, nextSlide, isInteracting, showDisplacementEffects, activateFilterEffects, scheduler, setFiltersActive ]); const handlePrev = useCallback(() => { if (!appRef.current || !isAppReady || slidesRef.current.length === 0) return; const prevIndex = (currentSlideIndex - 1 + slidesRef.current.length) % slidesRef.current.length; scheduler.scheduleTypedUpdate( "slider", UpdateType.SLIDE_TRANSITION, () => { prevSlide(prevIndex); currentIndexRef.current = prevIndex; setCurrentSlideIndex(prevIndex); setTimeout(() => { console.log("Scheduling effects after slide change (prev)"); scheduler.scheduleTypedUpdate( "slider", UpdateType.DISPLACEMENT_EFFECT, () => { showDisplacementEffects(); }, // Use critical priority if cursor is within the canvas isInteracting ? "critical" : void 0 ); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { currentIndexRef.current = prevIndex; if (typeof activateFilterEffects === "function") { activateFilterEffects(); if (isInteracting) { setFiltersActive(true); } } }, // Use critical priority if cursor is within the canvas isInteracting ? "critical" : void 0 ); }, 100); } ); }, [ appRef, isAppReady, slidesRef, currentSlideIndex, prevSlide, isInteracting, showDisplacementEffects, activateFilterEffects, scheduler, setFiltersActive ]); useEffect(() => { if (!appRef.current || !isAppReady) return; currentIndexRef.current = currentSlideIndex; }, [appRef.current, currentSlideIndex, isAppReady]); useNavigation({ onNext: handleNext, onPrev: handlePrev, enableKeyboardNav: true }); useExternalNav({ externalNav, navElement, handleNext, handlePrev }); useTouchSwipe({ sliderRef, onSwipeLeft: handleNext, onSwipeRight: handlePrev }); useMouseDrag({ sliderRef, slidesRef, currentIndex: currentIndexRef, swipeScaleIntensity, swipeDistance: typeof window !== "undefined" ? window.innerWidth * 0.2 : 200, onSwipeLeft: handleNext, onSwipeRight: handlePrev }); useTextTilt({ sliderRef, textContainersRef, currentIndex: currentIndexRef, cursorTextEffect, maxContainerShiftFraction, bgDispFilterRef, cursorDispFilterRef, cursorImgEffect }); useResizeHandler({ sliderRef, appRef, slidesRef, textContainersRef, backgroundDisplacementSpriteRef, cursorDisplacementSpriteRef }); 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 }); cursorActiveRef.current = true; setIsInteracting(true); scheduler.scheduleTypedUpdate( "slider", UpdateType.DISPLACEMENT_EFFECT, () => { showDisplacementEffects(); console.log("Displacement effects activated"); }, "critical" ); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { if (!filtersInitialized && typeof activateFilterEffects === "function") { console.log("Filters not initialized, initializing now"); activateFilterEffects(); } else if (typeof activateFilterEffects === "function") { console.log("Using activateFilterEffects function for slide", currentSlideIndex); activateFilterEffects(); } else if (typeof updateFilterIntensities === "function") { console.log("Using updateFilterIntensities function with force=true"); updateFilterIntensities(true, true); } setFiltersActive(true); console.log("Filter activation completed with critical priority"); }, "critical" ); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { console.log("Final render update after mouse enter completed"); 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 ]); const handleMouseLeave = useCallback(() => { if (!isAppReady) return; setIsInteracting(false); 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); updateFilterIntensities(false, true); scheduler.scheduleTypedUpdate( "slider", UpdateType.FILTER_UPDATE, () => { }, "critical" ); }, [isAppReady, setIsInteracting, updateFilterIntensities, scheduler]); useEffect(() => { return () => { resetAllFilters(); if (resourceManagerRef.current) { resourceManagerRef.current.clearPendingUpdates(); } 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]); useEffect(() => { const handleError = (error) => { console.error("Filter system error:", error); resetAllFilters(); hideDisplacementEffects(); }; window.addEventListener("error", (e) => handleError(e.error)); return () => window.removeEventListener("error", (e) => handleError(e.error)); }, [resetAllFilters, hideDisplacementEffects]); useEffect(() => { return () => { if (resourceManagerRef.current) { resourceManagerRef.current.clearPendingUpdates(); resourceManagerRef.current.markUnmounting(); } }; }, []); useEffect(() => { if (!resourceManagerRef.current) return; let frameCount = 0; let lastTime = performance.now(); let fps = 60; const monitorPerformance = () => { frameCount++; const currentTime = performance.now(); const elapsed = currentTime - lastTime; if (elapsed > 1e3) { fps = frameCount * 1e3 / elapsed; frameCount = 0; lastTime = currentTime; if (resourceManagerRef.current && fps < 55) { resourceManagerRef.current.autoOptimizeFilters(fps, 55); } } performanceMonitorId = requestAnimationFrame(monitorPerformance); }; let performanceMonitorId = requestAnimationFrame(monitorPerformance); return () => { if (performanceMonitorId) { cancelAnimationFrame(performanceMonitorId); } }; }, [resourceManagerRef]); return /* @__PURE__ */ jsxs( "div", { className: styles.kineticSlider, ref: sliderRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [ (!isAppReady || !assetsLoaded) && /* @__PURE__ */ jsx("div", { className: styles.placeholder, children: /* @__PURE__ */ jsxs("div", { className: styles.loadingIndicator, children: [ /* @__PURE__ */ jsx("div", { className: styles.spinner }), /* @__PURE__ */ jsx("div", { children: "Loading slider..." }) ] }) }), !externalNav && isClient && /* @__PURE__ */ jsxs("nav", { children: [ /* @__PURE__ */ jsx("button", { onClick: handlePrev, className: styles.prev, children: "Prev" }), /* @__PURE__ */ jsx("button", { onClick: handleNext, className: styles.next, children: "Next" }) ] }) ] } ); }; export { KineticSlider as default }; //# sourceMappingURL=KineticSlider.js.map