UNPKG

unicornstudio-react

Version:

React component for embedding Unicorn.Studio interactive scenes with TypeScript support. Compatible with React (Vite) and Next.js.

426 lines (416 loc) 13.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var src_exports = {}; __export(src_exports, { UnicornScene: () => UnicornScene, default: () => react_default }); module.exports = __toCommonJS(src_exports); // src/react/index.tsx var import_react3 = require("react"); // src/react/hooks.ts var import_react2 = require("react"); // src/shared/hooks.ts var import_react = require("react"); // src/shared/constants.ts var UNICORN_STUDIO_VERSION = "1.4.29"; var UNICORN_STUDIO_CDN_URL = `https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js@v${UNICORN_STUDIO_VERSION}/dist/unicornStudio.umd.js`; var DEFAULT_VALUES = { width: "100%", height: "100%", scale: 1, // 0.25 to 1.0 production: true, dpi: 1.5, fps: 60, // 15, 24, 30, 60, or 120 altText: "Scene", className: "", lazyLoad: true, showPlaceholderOnError: true, showPlaceholderWhileLoading: true }; var VALID_FPS = [15, 24, 30, 60, 120]; // src/shared/utils.ts function isWebGLSupported() { if (typeof window === "undefined") return true; try { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); return !!gl; } catch (e) { return false; } } function validateFPS(fps) { return VALID_FPS.includes(fps); } function validateScale(scale) { return scale >= 0.25 && scale <= 1; } function validateParameters(scale, fps) { if (!validateScale(scale)) { return `Invalid scale: ${scale}. Scale must be between 0.25 and 1.0`; } if (!validateFPS(fps)) { return `Invalid fps: ${fps}. FPS must be one of: 15, 24, 30, 60, 120`; } return null; } // src/shared/hooks.ts function useUnicornScene({ elementRef, projectId, jsonFilePath, production, scale, dpi, fps, lazyLoad, altText, ariaLabel, isScriptLoaded, onLoad, onError }) { const sceneRef = (0, import_react.useRef)(null); const [initError, setInitError] = (0, import_react.useState)(null); const hasAttemptedRef = (0, import_react.useRef)(false); const initializationKeyRef = (0, import_react.useRef)(""); const isInitializingRef = (0, import_react.useRef)(false); const validationError = (0, import_react.useMemo)(() => { return validateParameters(scale, fps); }, [scale, fps]); const prevValidationError = (0, import_react.useRef)(null); (0, import_react.useEffect)(() => { if (validationError !== prevValidationError.current) { prevValidationError.current = validationError; if (validationError) { const error = new Error(validationError); setInitError(error); onError == null ? void 0 : onError(error); } else { setInitError(null); } } }, [validationError, onError]); const destroyScene = (0, import_react.useCallback)(() => { var _a; if ((_a = sceneRef.current) == null ? void 0 : _a.destroy) { sceneRef.current.destroy(); sceneRef.current = null; } isInitializingRef.current = false; }, []); const initializeScene = (0, import_react.useCallback)(async () => { var _a; if (!elementRef.current || !isScriptLoaded || validationError) return; if (isInitializingRef.current) { console.log("Already initializing, skipping..."); return; } const currentKey = `${projectId || ""}-${jsonFilePath || ""}-${scale}-${dpi}-${fps}-${production ? "prod" : "dev"}`; if (initializationKeyRef.current === currentKey && sceneRef.current) { console.log("Scene already initialized with this configuration, skipping..."); return; } initializationKeyRef.current = currentKey; hasAttemptedRef.current = true; isInitializingRef.current = true; try { destroyScene(); if (!((_a = window.UnicornStudio) == null ? void 0 : _a.addScene)) { throw new Error("UnicornStudio.addScene not found"); } const sceneConfig = { elementId: elementRef.current.id || `unicorn-${Math.random().toString(36).slice(2, 11)}`, scale, dpi, fps, lazyLoad, altText, ariaLabel, production }; if (!elementRef.current.id) { elementRef.current.id = sceneConfig.elementId; } if (jsonFilePath) { sceneConfig.filePath = jsonFilePath; } else if (projectId) { sceneConfig.projectId = projectId; } else { throw new Error("No project ID or JSON file path provided"); } let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout( () => reject(new Error("Scene initialization timeout")), 15e3 ); }); const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); } }; try { const scene = await Promise.race([ window.UnicornStudio.addScene(sceneConfig), timeoutPromise ]); cleanup(); if (scene) { sceneRef.current = scene; hasAttemptedRef.current = false; setInitError(null); isInitializingRef.current = false; onLoad == null ? void 0 : onLoad(); } else { isInitializingRef.current = false; throw new Error("Failed to initialize scene"); } } catch (error) { cleanup(); throw error; } } catch (error) { const err = error instanceof Error ? error : new Error("Unknown error"); let sanitizedMessage = err.message; if (sanitizedMessage.includes("404") || sanitizedMessage.includes("Failed to fetch")) { sanitizedMessage = "Resource not found"; } else if (sanitizedMessage.includes("Network") || sanitizedMessage.includes("network")) { sanitizedMessage = "Network error occurred"; } else if (sanitizedMessage.includes("timeout")) { sanitizedMessage = "Loading timeout"; } const sanitizedError = new Error(sanitizedMessage); setInitError(sanitizedError); isInitializingRef.current = false; onError == null ? void 0 : onError(sanitizedError); } }, [ elementRef, isScriptLoaded, jsonFilePath, projectId, production, scale, dpi, fps, lazyLoad, altText, ariaLabel, destroyScene, onLoad, onError, validationError ]); (0, import_react.useEffect)(() => { if (isScriptLoaded) { void initializeScene(); } }, [isScriptLoaded, initializeScene]); (0, import_react.useEffect)(() => { return destroyScene; }, [destroyScene]); (0, import_react.useEffect)(() => { const newKey = `${projectId || ""}-${jsonFilePath || ""}-${scale}-${dpi}-${fps}-${production ? "prod" : "dev"}`; if (initializationKeyRef.current !== newKey) { hasAttemptedRef.current = false; setInitError(null); isInitializingRef.current = false; initializationKeyRef.current = ""; } }, [projectId, jsonFilePath, scale, dpi, fps, production]); return { error: initError }; } // src/react/hooks.ts function useUnicornStudioScript(scriptUrl) { const [isLoaded, setIsLoaded] = (0, import_react2.useState)(false); const [error, setError] = (0, import_react2.useState)(null); const handleScriptLoad = (0, import_react2.useCallback)(() => { setIsLoaded(true); }, []); const handleScriptError = (0, import_react2.useCallback)(() => { setError(new Error("Failed to load UnicornStudio script")); }, []); (0, import_react2.useEffect)(() => { const existingScript = document.querySelector( `script[src="${scriptUrl}"]` ); if (existingScript) { if (existingScript.getAttribute("data-loaded") === "true") { setIsLoaded(true); return; } existingScript.addEventListener("load", handleScriptLoad); existingScript.addEventListener("error", handleScriptError); return () => { existingScript.removeEventListener("load", handleScriptLoad); existingScript.removeEventListener("error", handleScriptError); }; } const script = document.createElement("script"); script.src = scriptUrl; script.async = true; script.addEventListener("load", () => { script.setAttribute("data-loaded", "true"); handleScriptLoad(); }); script.addEventListener("error", handleScriptError); document.head.appendChild(script); return () => { if (script.parentNode) { script.removeEventListener("load", handleScriptLoad); script.removeEventListener("error", handleScriptError); } }; }, [scriptUrl, handleScriptLoad, handleScriptError]); return { isLoaded, error, handleScriptLoad, handleScriptError }; } // src/shared/styles.ts var unicornStyles = { container: { position: "relative", width: "var(--unicorn-width)", height: "var(--unicorn-height)" }, errorWrapper: { display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }, errorBox: { textAlign: "center", padding: "1rem", borderRadius: "0.5rem", backgroundColor: "rgb(254 242 242)", color: "rgb(239 68 68)" }, errorTitle: { fontWeight: "600", marginBottom: "0.25rem" }, errorMessage: { fontSize: "0.875rem", marginTop: "0.25rem" } }; // src/react/index.tsx var import_jsx_runtime = require("react/jsx-runtime"); function UnicornScene({ projectId, jsonFilePath, width = DEFAULT_VALUES.width, height = DEFAULT_VALUES.height, scale = DEFAULT_VALUES.scale, dpi = DEFAULT_VALUES.dpi, fps = DEFAULT_VALUES.fps, altText = DEFAULT_VALUES.altText, ariaLabel, className = DEFAULT_VALUES.className, lazyLoad = DEFAULT_VALUES.lazyLoad, production = DEFAULT_VALUES.production, placeholder, placeholderClassName, showPlaceholderOnError = DEFAULT_VALUES.showPlaceholderOnError, showPlaceholderWhileLoading = DEFAULT_VALUES.showPlaceholderWhileLoading, onLoad, onError }) { const elementRef = (0, import_react3.useRef)(null); const [isSceneLoaded, setIsSceneLoaded] = (0, import_react3.useState)(false); const [webGLSupported, setWebGLSupported] = (0, import_react3.useState)(true); const { isLoaded, error: scriptError } = useUnicornStudioScript(UNICORN_STUDIO_CDN_URL); const { error: sceneError } = useUnicornScene({ elementRef, projectId, jsonFilePath, production, scale, dpi, fps, lazyLoad, altText, ariaLabel: ariaLabel || altText, isScriptLoaded: isLoaded, onLoad: () => { setIsSceneLoaded(true); onLoad == null ? void 0 : onLoad(); }, onError }); const error = scriptError || sceneError; (0, import_react3.useEffect)(() => { setWebGLSupported(isWebGLSupported()); }, []); const showPlaceholder = (placeholder || placeholderClassName) && (!webGLSupported || showPlaceholderWhileLoading && !isSceneLoaded || showPlaceholderOnError && error); const numericWidth = typeof width === "number" ? width : 0; const numericHeight = typeof height === "number" ? height : 0; const useNumericDimensions = typeof width === "number" && typeof height === "number"; const customProperties = { "--unicorn-width": typeof width === "number" ? `${width}px` : width, "--unicorn-height": typeof height === "number" ? `${height}px` : height }; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( "div", { ref: elementRef, style: { ...unicornStyles.container, ...customProperties }, className, children: [ showPlaceholder && (placeholder || placeholderClassName) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0 }, children: typeof placeholder === "string" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "img", { src: placeholder, alt: altText, style: { width: useNumericDimensions ? `${numericWidth}px` : "100%", height: useNumericDimensions ? `${numericHeight}px` : "100%", objectFit: "cover" } } ) : placeholder ? placeholder : placeholderClassName ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: placeholderClassName, style: { width: "100%", height: "100%" }, "aria-label": altText } ) : null }), error && !showPlaceholder && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: unicornStyles.errorWrapper, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: unicornStyles.errorBox, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: unicornStyles.errorTitle, children: "Error loading scene" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: unicornStyles.errorMessage, children: error.message }) ] }) }) ] } ); } var react_default = UnicornScene; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { UnicornScene }); //# sourceMappingURL=index.js.map