@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
JavaScript
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