Spaces:
Sleeping
Sleeping
| "use client"; | |
| // src/next/index.tsx | |
| import { useRef as useRef2, useState as useState3, useEffect as useEffect3 } from "react"; | |
| import Script from "next/script"; | |
| import Image from "next/image"; | |
| // src/next/hooks.ts | |
| import { useState as useState2, useCallback as useCallback2, useEffect as useEffect2 } from "react"; | |
| // src/shared/hooks.ts | |
| import { useEffect, useRef, useState, useCallback, useMemo } from "react"; | |
| // src/shared/constants.ts | |
| var UNICORN_STUDIO_VERSION = "2.1.4"; | |
| var UNICORN_STUDIO_CDN_URL = `https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js@v${UNICORN_STUDIO_VERSION}/dist/unicornStudio.umd.js`; | |
| var DEFAULT_VALUES = { | |
| /** Default container width */ | |
| width: "100%", | |
| /** Default container height */ | |
| height: "100%", | |
| /** Default rendering scale (0.25 to 1.0) */ | |
| scale: 1, | |
| /** Default production mode setting */ | |
| production: true, | |
| /** Default device pixel ratio */ | |
| dpi: 1.5, | |
| /** Default frames per second */ | |
| fps: 60, | |
| /** Default alt text for accessibility */ | |
| altText: "Scene", | |
| /** Default CSS class name */ | |
| className: "", | |
| /** Default paused state */ | |
| paused: false, | |
| /** Default lazy loading setting */ | |
| lazyLoad: true, | |
| /** Default setting for showing placeholder on error */ | |
| showPlaceholderOnError: true, | |
| /** Default setting for showing placeholder while loading */ | |
| 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 sanitizeErrorMessage(message) { | |
| if (message.includes("404") || message.includes("Failed to fetch")) { | |
| return "Resource not found"; | |
| } | |
| if (message.includes("Network") || message.includes("network")) { | |
| return "Network error occurred"; | |
| } | |
| if (message.includes("timeout")) { | |
| return "Loading timeout"; | |
| } | |
| const urlPattern = /\bhttps?:\/\/[^\s)]+/gi; | |
| const filePathPattern = /\b(?:[A-Za-z]:\\|\/)[^\s)]+/g; | |
| let sanitized = message.replaceAll(urlPattern, "[redacted]"); | |
| sanitized = sanitized.replaceAll(filePathPattern, "[redacted]"); | |
| return sanitized; | |
| } | |
| function buildSceneConfig(element, params) { | |
| const elementId = element.id || `unicorn-${Math.random().toString(36).slice(2, 11)}`; | |
| if (!element.id) { | |
| element.id = elementId; | |
| } | |
| const config = { | |
| elementId, | |
| scale: params.scale, | |
| dpi: params.dpi, | |
| fps: params.fps, | |
| lazyLoad: params.lazyLoad, | |
| altText: params.altText, | |
| ariaLabel: params.ariaLabel, | |
| production: params.production | |
| }; | |
| if (params.jsonFilePath) { | |
| config.filePath = params.jsonFilePath; | |
| } else if (params.projectId) { | |
| config.projectId = params.projectId; | |
| } else { | |
| throw new Error("No project ID or JSON file path provided"); | |
| } | |
| return config; | |
| } | |
| var INIT_TIMEOUT_MS = 15e3; | |
| async function withTimeout(promise, ms = INIT_TIMEOUT_MS) { | |
| let timeoutId; | |
| const timeoutPromise = new Promise((_, reject) => { | |
| timeoutId = setTimeout( | |
| () => reject(new Error("Scene initialization timeout")), | |
| ms | |
| ); | |
| }); | |
| try { | |
| return await Promise.race([promise, timeoutPromise]); | |
| } finally { | |
| if (timeoutId) clearTimeout(timeoutId); | |
| } | |
| } | |
| function assignSceneRef(ref, value) { | |
| if (!ref) return; | |
| if (typeof ref === "function") { | |
| ref(value); | |
| return; | |
| } | |
| ref.current = value; | |
| } | |
| function useUnicornScene({ | |
| elementRef, | |
| projectId, | |
| jsonFilePath, | |
| production, | |
| scale, | |
| dpi, | |
| fps, | |
| lazyLoad, | |
| altText, | |
| ariaLabel, | |
| isScriptLoaded, | |
| paused, | |
| onLoad, | |
| onError, | |
| sceneRef | |
| }) { | |
| const internalSceneRef = useRef(null); | |
| const [initError, setInitError] = useState(null); | |
| const initializationKeyRef = useRef(""); | |
| const isInitializingRef = useRef(false); | |
| const onLoadRef = useRef(onLoad); | |
| onLoadRef.current = onLoad; | |
| const onErrorRef = useRef(onError); | |
| onErrorRef.current = onError; | |
| const sceneRefRef = useRef(sceneRef); | |
| const prevSceneRef = useRef(sceneRef); | |
| if (sceneRef !== prevSceneRef.current) { | |
| assignSceneRef(prevSceneRef.current, null); | |
| sceneRefRef.current = sceneRef; | |
| prevSceneRef.current = sceneRef; | |
| assignSceneRef(sceneRef, internalSceneRef.current); | |
| } | |
| const validationError = useMemo(() => { | |
| return validateParameters(scale, fps); | |
| }, [scale, fps]); | |
| const prevValidationError = useRef(null); | |
| useEffect(() => { | |
| var _a; | |
| if (validationError !== prevValidationError.current) { | |
| prevValidationError.current = validationError; | |
| if (validationError) { | |
| const error = new Error(validationError); | |
| setInitError(error); | |
| (_a = onErrorRef.current) == null ? void 0 : _a.call(onErrorRef, error); | |
| } else { | |
| setInitError(null); | |
| } | |
| } | |
| }, [validationError]); | |
| const destroyScene = useCallback(() => { | |
| var _a; | |
| if ((_a = internalSceneRef.current) == null ? void 0 : _a.destroy) { | |
| internalSceneRef.current.destroy(); | |
| internalSceneRef.current = null; | |
| assignSceneRef(sceneRefRef.current, null); | |
| } | |
| isInitializingRef.current = false; | |
| }, []); | |
| useEffect(() => { | |
| let ignore = false; | |
| async function initializeScene() { | |
| var _a, _b, _c; | |
| if (!elementRef.current || !isScriptLoaded || validationError) return; | |
| if (isInitializingRef.current) return; | |
| const currentKey = `${projectId || ""}-${jsonFilePath || ""}-${scale}-${dpi}-${fps}-${production ? "prod" : "dev"}`; | |
| if (initializationKeyRef.current === currentKey && internalSceneRef.current) { | |
| return; | |
| } | |
| initializationKeyRef.current = currentKey; | |
| try { | |
| destroyScene(); | |
| isInitializingRef.current = true; | |
| if (!((_a = window.UnicornStudio) == null ? void 0 : _a.addScene)) { | |
| throw new Error("UnicornStudio.addScene not found"); | |
| } | |
| const sceneConfig = buildSceneConfig(elementRef.current, { | |
| jsonFilePath, | |
| projectId, | |
| scale, | |
| dpi, | |
| fps, | |
| lazyLoad, | |
| altText, | |
| ariaLabel, | |
| production | |
| }); | |
| const scene = await withTimeout( | |
| window.UnicornStudio.addScene(sceneConfig) | |
| ); | |
| if (ignore) { | |
| scene == null ? void 0 : scene.destroy(); | |
| return; | |
| } | |
| if (scene) { | |
| internalSceneRef.current = scene; | |
| assignSceneRef(sceneRefRef.current, scene); | |
| setInitError(null); | |
| isInitializingRef.current = false; | |
| (_b = onLoadRef.current) == null ? void 0 : _b.call(onLoadRef); | |
| } else { | |
| isInitializingRef.current = false; | |
| throw new Error("Failed to initialize scene"); | |
| } | |
| } catch (error) { | |
| if (ignore) return; | |
| const err = error instanceof Error ? error : new Error("Unknown error"); | |
| const sanitizedError = new Error(sanitizeErrorMessage(err.message)); | |
| setInitError(sanitizedError); | |
| isInitializingRef.current = false; | |
| (_c = onErrorRef.current) == null ? void 0 : _c.call(onErrorRef, sanitizedError); | |
| } | |
| } | |
| if (isScriptLoaded) { | |
| void initializeScene(); | |
| } | |
| return () => { | |
| ignore = true; | |
| destroyScene(); | |
| }; | |
| }, [ | |
| isScriptLoaded, | |
| elementRef, | |
| jsonFilePath, | |
| projectId, | |
| production, | |
| scale, | |
| dpi, | |
| fps, | |
| lazyLoad, | |
| altText, | |
| ariaLabel, | |
| destroyScene, | |
| validationError | |
| ]); | |
| useEffect(() => { | |
| if (internalSceneRef.current && paused !== void 0) { | |
| internalSceneRef.current.paused = paused; | |
| } | |
| }, [paused]); | |
| useEffect(() => { | |
| const el = elementRef.current; | |
| if (!el || typeof ResizeObserver === "undefined") return; | |
| const observer = new ResizeObserver(() => { | |
| var _a, _b; | |
| (_b = (_a = internalSceneRef.current) == null ? void 0 : _a.resize) == null ? void 0 : _b.call(_a); | |
| }); | |
| observer.observe(el); | |
| return () => { | |
| observer.disconnect(); | |
| }; | |
| }, [elementRef]); | |
| return { error: initError }; | |
| } | |
| // src/next/hooks.ts | |
| function useUnicornStudioScript() { | |
| const [isLoaded, setIsLoaded] = useState2(false); | |
| const [error, setError] = useState2(null); | |
| const handleScriptLoad = useCallback2(() => { | |
| setIsLoaded((prev) => { | |
| if (!prev && typeof window !== "undefined" && window.UnicornStudio) { | |
| setError(null); | |
| return true; | |
| } | |
| return prev; | |
| }); | |
| }, []); | |
| const handleScriptError = useCallback2(() => { | |
| setError(new Error("Failed to load UnicornStudio script")); | |
| setIsLoaded(false); | |
| }, []); | |
| useEffect2(() => { | |
| if (typeof window !== "undefined" && window.UnicornStudio && !isLoaded) { | |
| setIsLoaded(true); | |
| setError(null); | |
| } | |
| }, [isLoaded]); | |
| return { isLoaded, error, handleScriptLoad, handleScriptError }; | |
| } | |
| // src/shared/styles.ts | |
| var unicornStyles = { | |
| /** | |
| * Styles for the main scene container element. | |
| * | |
| * @remarks | |
| * Uses CSS custom properties for width and height to allow dynamic sizing. | |
| */ | |
| container: { | |
| position: "relative", | |
| width: "var(--unicorn-width)", | |
| height: "var(--unicorn-height)" | |
| }, | |
| /** | |
| * Styles for the error message wrapper. | |
| */ | |
| errorWrapper: { | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| height: "100%" | |
| }, | |
| /** | |
| * Styles for the error message box. | |
| */ | |
| errorBox: { | |
| textAlign: "center", | |
| padding: "1rem", | |
| borderRadius: "0.5rem", | |
| backgroundColor: "rgb(254 242 242)", | |
| color: "rgb(239 68 68)" | |
| }, | |
| /** | |
| * Styles for the error title text. | |
| */ | |
| errorTitle: { | |
| fontWeight: "600", | |
| marginBottom: "0.25rem" | |
| }, | |
| /** | |
| * Styles for the error message text. | |
| */ | |
| errorMessage: { | |
| fontSize: "0.875rem", | |
| marginTop: "0.25rem" | |
| } | |
| }; | |
| // src/next/index.tsx | |
| import { Fragment, jsx, jsxs } from "react/jsx-runtime"; | |
| function UnicornScene({ | |
| projectId, | |
| jsonFilePath, | |
| sdkUrl = UNICORN_STUDIO_CDN_URL, | |
| 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, | |
| paused = DEFAULT_VALUES.paused, | |
| placeholder, | |
| placeholderClassName, | |
| showPlaceholderOnError = DEFAULT_VALUES.showPlaceholderOnError, | |
| showPlaceholderWhileLoading = DEFAULT_VALUES.showPlaceholderWhileLoading, | |
| onLoad, | |
| onError, | |
| sceneRef | |
| }) { | |
| const elementRef = useRef2(null); | |
| const [isSceneLoaded, setIsSceneLoaded] = useState3(false); | |
| const [webGLSupported, setWebGLSupported] = useState3(true); | |
| const { | |
| isLoaded, | |
| error: scriptError, | |
| handleScriptLoad, | |
| handleScriptError | |
| } = useUnicornStudioScript(); | |
| const { error: sceneError } = useUnicornScene({ | |
| elementRef, | |
| projectId, | |
| jsonFilePath, | |
| production, | |
| scale, | |
| dpi, | |
| fps, | |
| lazyLoad, | |
| altText, | |
| ariaLabel: ariaLabel || altText, | |
| isScriptLoaded: isLoaded, | |
| paused, | |
| sceneRef, | |
| onLoad: () => { | |
| setIsSceneLoaded(true); | |
| onLoad == null ? void 0 : onLoad(); | |
| }, | |
| onError | |
| }); | |
| const error = scriptError || sceneError; | |
| useEffect3(() => { | |
| 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__ */ jsxs(Fragment, { children: [ | |
| /* @__PURE__ */ jsx( | |
| Script, | |
| { | |
| src: sdkUrl, | |
| strategy: lazyLoad ? "lazyOnload" : "afterInteractive", | |
| onLoad: handleScriptLoad, | |
| onError: handleScriptError | |
| } | |
| ), | |
| /* @__PURE__ */ jsxs( | |
| "div", | |
| { | |
| ref: elementRef, | |
| style: { ...unicornStyles.container, ...customProperties }, | |
| className, | |
| children: [ | |
| showPlaceholder && (placeholder || placeholderClassName) && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0 }, children: typeof placeholder === "string" ? useNumericDimensions ? /* @__PURE__ */ jsx( | |
| Image, | |
| { | |
| src: placeholder, | |
| alt: altText, | |
| width: numericWidth, | |
| height: numericHeight, | |
| style: { objectFit: "cover" }, | |
| priority: true | |
| } | |
| ) : /* @__PURE__ */ jsx( | |
| Image, | |
| { | |
| src: placeholder, | |
| alt: altText, | |
| fill: true, | |
| style: { objectFit: "cover" }, | |
| priority: true | |
| } | |
| ) : placeholder ? placeholder : placeholderClassName ? /* @__PURE__ */ jsx( | |
| "div", | |
| { | |
| className: placeholderClassName, | |
| style: { width: "100%", height: "100%" }, | |
| "aria-label": altText | |
| } | |
| ) : null }), | |
| error && !showPlaceholder && /* @__PURE__ */ jsx("div", { style: unicornStyles.errorWrapper, children: /* @__PURE__ */ jsxs("div", { style: unicornStyles.errorBox, children: [ | |
| /* @__PURE__ */ jsx("p", { style: unicornStyles.errorTitle, children: "Error loading scene" }), | |
| /* @__PURE__ */ jsx("p", { style: unicornStyles.errorMessage, children: error.message }) | |
| ] }) }) | |
| ] | |
| } | |
| ) | |
| ] }); | |
| } | |
| var next_default = UnicornScene; | |
| export { | |
| UnicornScene, | |
| next_default as default | |
| }; | |
| //# sourceMappingURL=next.mjs.map |