UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

142 lines (141 loc) 4.87 kB
import { toMotionGPUErrorReport } from "../core/error-report.js"; import { createCurrentWritable } from "../core/current-value.js"; import { isAbortError, loadTexturesFromUrls } from "../core/texture-loader.js"; import { useCallback, useEffect, useRef } from "react"; //#region src/lib/react/use-texture.ts /** * Normalizes unknown thrown values to an `Error` instance. */ function toError(error) { if (error instanceof Error) return error; return /* @__PURE__ */ new Error("Unknown texture loading error"); } /** * Releases GPU-side resources for a list of loaded textures. */ function disposeTextures(list) { for (const texture of list ?? []) texture.dispose(); } function mergeAbortSignals(primary, secondary) { if (!secondary) return { signal: primary, dispose: () => {} }; if (typeof AbortSignal.any === "function") return { signal: AbortSignal.any([primary, secondary]), dispose: () => {} }; const fallback = new AbortController(); let disposed = false; const cleanup = () => { if (disposed) return; disposed = true; primary.removeEventListener("abort", abort); secondary.removeEventListener("abort", abort); }; const abort = () => fallback.abort(); primary.addEventListener("abort", abort, { once: true }); secondary.addEventListener("abort", abort, { once: true }); return { signal: fallback.signal, dispose: cleanup }; } /** * Loads textures from URLs and exposes reactive loading/error state. * * @param urlInput - URLs array or lazy URL provider. * @param options - Loader options passed to URL fetch/decode pipeline. * @returns Reactive texture loading state with reload support. */ function useTexture(urlInput, options = {}) { const texturesRef = useRef(createCurrentWritable(null)); const loadingRef = useRef(createCurrentWritable(true)); const errorRef = useRef(createCurrentWritable(null)); const errorReportRef = useRef(createCurrentWritable(null)); const activeControllerRef = useRef(null); const runningLoadRef = useRef(null); const reloadQueuedRef = useRef(false); const requestVersionRef = useRef(0); const disposedRef = useRef(false); const optionsRef = useRef(options); const urlInputRef = useRef(urlInput); optionsRef.current = options; urlInputRef.current = urlInput; const getUrls = useCallback(() => { const currentInput = urlInputRef.current; return typeof currentInput === "function" ? currentInput() : currentInput; }, []); const executeLoad = useCallback(async () => { if (disposedRef.current) return; const version = ++requestVersionRef.current; const controller = new AbortController(); activeControllerRef.current = controller; loadingRef.current.set(true); errorRef.current.set(null); errorReportRef.current.set(null); const previous = texturesRef.current.current; const mergedSignal = mergeAbortSignals(controller.signal, optionsRef.current.signal); try { const loaded = await loadTexturesFromUrls(getUrls(), { ...optionsRef.current, signal: mergedSignal.signal }); if (disposedRef.current || version !== requestVersionRef.current) { disposeTextures(loaded); return; } texturesRef.current.set(loaded); disposeTextures(previous); } catch (nextError) { if (disposedRef.current || version !== requestVersionRef.current) return; if (isAbortError(nextError)) return; disposeTextures(previous); texturesRef.current.set(null); const normalizedError = toError(nextError); errorRef.current.set(normalizedError); errorReportRef.current.set(toMotionGPUErrorReport(normalizedError, "initialization")); } finally { if (!disposedRef.current && version === requestVersionRef.current) loadingRef.current.set(false); if (activeControllerRef.current === controller) activeControllerRef.current = null; mergedSignal.dispose(); } }, [getUrls]); const runLoadLoop = useCallback(async () => { do { reloadQueuedRef.current = false; await executeLoad(); } while (reloadQueuedRef.current && !disposedRef.current); }, [executeLoad]); const load = useCallback(() => { activeControllerRef.current?.abort(); if (runningLoadRef.current) { reloadQueuedRef.current = true; return runningLoadRef.current; } const trackedPending = runLoadLoop().finally(() => { if (runningLoadRef.current === trackedPending) runningLoadRef.current = null; }); runningLoadRef.current = trackedPending; return trackedPending; }, [runLoadLoop]); useEffect(() => { load(); return () => { disposedRef.current = true; requestVersionRef.current += 1; activeControllerRef.current?.abort(); disposeTextures(texturesRef.current.current); }; }, [load]); return { textures: texturesRef.current, loading: loadingRef.current, error: errorRef.current, errorReport: errorReportRef.current, reload: load }; } //#endregion export { useTexture }; //# sourceMappingURL=use-texture.js.map