UNPKG

@motion-core/motion-gpu

Version:

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

231 lines (230 loc) 8.1 kB
//#region src/lib/core/texture-loader.ts var resourceCache = /* @__PURE__ */ new Map(); function createAbortError() { try { return new DOMException("Texture request was aborted", "AbortError"); } catch { const error = /* @__PURE__ */ new Error("Texture request was aborted"); error.name = "AbortError"; return error; } } /** * Checks whether error represents abort cancellation. */ function isAbortError(error) { return error instanceof Error && (error.name === "AbortError" || error.message.toLowerCase().includes("aborted")); } function toBodyFingerprint(body) { if (body == null) return null; if (typeof body === "string") return `string:${body}`; if (body instanceof URLSearchParams) return `urlsearchparams:${body.toString()}`; if (typeof FormData !== "undefined" && body instanceof FormData) return `formdata:${Array.from(body.entries()).map(([key, value]) => `${key}:${String(value)}`).join("&")}`; if (body instanceof Blob) return `blob:${body.type}:${body.size}`; if (body instanceof ArrayBuffer) return `arraybuffer:${body.byteLength}`; if (ArrayBuffer.isView(body)) return `view:${body.byteLength}`; return `opaque:${Object.prototype.toString.call(body)}`; } function normalizeRequestInit(requestInit) { if (!requestInit) return {}; const headers = new Headers(requestInit.headers); const headerEntries = Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b)); const normalized = {}; normalized.method = (requestInit.method ?? "GET").toUpperCase(); normalized.mode = requestInit.mode ?? null; normalized.cache = requestInit.cache ?? null; normalized.credentials = requestInit.credentials ?? null; normalized.redirect = requestInit.redirect ?? null; normalized.referrer = requestInit.referrer ?? null; normalized.referrerPolicy = requestInit.referrerPolicy ?? null; normalized.integrity = requestInit.integrity ?? null; normalized.keepalive = requestInit.keepalive ?? false; normalized.priority = requestInit.priority ?? null; normalized.headers = headerEntries; normalized.body = toBodyFingerprint(requestInit.body); return normalized; } function normalizeTextureLoadOptions(options) { const colorSpace = options.colorSpace ?? "srgb"; const normalized = { colorSpace, decode: { colorSpaceConversion: options.decode?.colorSpaceConversion ?? (colorSpace === "linear" ? "none" : "default"), premultiplyAlpha: options.decode?.premultiplyAlpha ?? "default", imageOrientation: options.decode?.imageOrientation ?? "none" } }; if (options.requestInit !== void 0) normalized.requestInit = options.requestInit; if (options.signal !== void 0) normalized.signal = options.signal; if (options.update !== void 0) normalized.update = options.update; if (options.flipY !== void 0) normalized.flipY = options.flipY; if (options.premultipliedAlpha !== void 0) normalized.premultipliedAlpha = options.premultipliedAlpha; if (options.generateMipmaps !== void 0) normalized.generateMipmaps = options.generateMipmaps; return normalized; } /** * Builds deterministic resource cache key from full URL IO config. */ function buildTextureResourceCacheKey(url, options = {}) { const normalized = normalizeTextureLoadOptions(options); return JSON.stringify({ url, colorSpace: normalized.colorSpace, requestInit: normalizeRequestInit(normalized.requestInit), decode: normalized.decode }); } /** * Clears the internal texture resource cache. */ function clearTextureBlobCache() { for (const entry of resourceCache.values()) if (!entry.settled) entry.controller.abort(); resourceCache.clear(); } function acquireTextureBlob(url, options) { const key = buildTextureResourceCacheKey(url, options); const existing = resourceCache.get(key); if (existing) { existing.refs += 1; let released = false; return { entry: existing, release: () => { if (released) return; released = true; existing.refs = Math.max(0, existing.refs - 1); if (existing.refs === 0) { if (!existing.settled) existing.controller.abort(); resourceCache.delete(key); } } }; } const normalized = normalizeTextureLoadOptions(options); const controller = new AbortController(); const requestInit = { ...normalized.requestInit ?? {}, signal: controller.signal }; const entry = { key, refs: 1, controller, settled: false, blobPromise: fetch(url, requestInit).then(async (response) => { if (!response.ok) throw new Error(`Texture request failed (${response.status}) for ${url}`); return response.blob(); }).then((blob) => { entry.settled = true; return blob; }).catch((error) => { resourceCache.delete(key); throw error; }) }; resourceCache.set(key, entry); let released = false; return { entry, release: () => { if (released) return; released = true; entry.refs = Math.max(0, entry.refs - 1); if (entry.refs === 0) { if (!entry.settled) entry.controller.abort(); resourceCache.delete(key); } } }; } async function awaitWithAbort(promise, signal) { if (!signal) return promise; if (signal.aborted) throw createAbortError(); return new Promise((resolve, reject) => { const onAbort = () => { reject(createAbortError()); }; signal.addEventListener("abort", onAbort, { once: true }); promise.then(resolve, reject).finally(() => { signal.removeEventListener("abort", onAbort); }); }); } /** * Loads a single texture from URL and converts it to an `ImageBitmap`. * * @param url - Texture URL. * @param options - Loading options. * @returns Loaded texture object. * @throws {Error} When runtime does not support `createImageBitmap` or request fails. */ async function loadTextureFromUrl(url, options = {}) { if (typeof createImageBitmap !== "function") throw new Error("createImageBitmap is not available in this runtime"); const normalized = normalizeTextureLoadOptions(options); const { entry, release } = acquireTextureBlob(url, options); let bitmap = null; try { const blob = await awaitWithAbort(entry.blobPromise, normalized.signal); const bitmapOptions = { colorSpaceConversion: normalized.decode.colorSpaceConversion, premultiplyAlpha: normalized.decode.premultiplyAlpha, imageOrientation: normalized.decode.imageOrientation }; bitmap = bitmapOptions.colorSpaceConversion === "default" && bitmapOptions.premultiplyAlpha === "default" && bitmapOptions.imageOrientation === "none" ? await createImageBitmap(blob) : await createImageBitmap(blob, bitmapOptions); if (normalized.signal?.aborted) { bitmap.close(); throw createAbortError(); } let disposed = false; const loaded = { url, source: bitmap, width: bitmap.width, height: bitmap.height, colorSpace: normalized.colorSpace, dispose: () => { if (disposed) return; disposed = true; bitmap?.close(); bitmap = null; } }; if (normalized.update !== void 0) loaded.update = normalized.update; if (normalized.flipY !== void 0) loaded.flipY = normalized.flipY; if (normalized.premultipliedAlpha !== void 0) loaded.premultipliedAlpha = normalized.premultipliedAlpha; if (normalized.generateMipmaps !== void 0) loaded.generateMipmaps = normalized.generateMipmaps; return loaded; } catch (error) { if (bitmap) bitmap.close(); throw error; } finally { release(); } } /** * Loads many textures in parallel from URLs. * * @param urls - Texture URLs. * @param options - Shared loading options. * @returns Promise resolving to loaded textures in input order. */ async function loadTexturesFromUrls(urls, options = {}) { const settled = await Promise.allSettled(urls.map((url) => loadTextureFromUrl(url, options))); const loaded = []; let firstError = null; for (const entry of settled) { if (entry.status === "fulfilled") { loaded.push(entry.value); continue; } firstError ??= entry.reason; } if (firstError) { for (const texture of loaded) texture.dispose(); throw firstError; } return loaded; } //#endregion export { buildTextureResourceCacheKey, clearTextureBlobCache, isAbortError, loadTextureFromUrl, loadTexturesFromUrls }; //# sourceMappingURL=texture-loader.js.map