unicornstudio-react
Version:
React component for embedding Unicorn.Studio interactive scenes with TypeScript support. Compatible with React (Vite) and Next.js.
440 lines (431 loc) • 14.5 kB
JavaScript
;
"use client";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/next/index.tsx
var next_exports = {};
__export(next_exports, {
UnicornScene: () => UnicornScene,
default: () => next_default
});
module.exports = __toCommonJS(next_exports);
var import_react3 = require("react");
var import_script = __toESM(require("next/script"));
var import_image = __toESM(require("next/image"));
// src/next/hooks.ts
var import_react2 = require("react");
// src/shared/hooks.ts
var import_react = require("react");
// src/shared/constants.ts
var UNICORN_STUDIO_VERSION = "2.0.1";
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/next/hooks.ts
function useUnicornStudioScript() {
const [isLoaded, setIsLoaded] = (0, import_react2.useState)(false);
const [error, setError] = (0, import_react2.useState)(null);
const handleScriptLoad = (0, import_react2.useCallback)(() => {
setIsLoaded((prev) => {
if (!prev && typeof window !== "undefined" && window.UnicornStudio) {
setError(null);
return true;
}
return prev;
});
}, []);
const handleScriptError = (0, import_react2.useCallback)(() => {
setError(new Error("Failed to load UnicornStudio script"));
setIsLoaded(false);
}, []);
(0, import_react2.useEffect)(() => {
if (typeof window !== "undefined" && window.UnicornStudio && !isLoaded) {
setIsLoaded(true);
setError(null);
}
}, [isLoaded]);
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/next/index.tsx
var import_jsx_runtime = require("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,
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,
handleScriptLoad,
handleScriptError
} = useUnicornStudioScript();
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)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_script.default,
{
src: sdkUrl,
strategy: lazyLoad ? "lazyOnload" : "afterInteractive",
onLoad: handleScriptLoad,
onError: handleScriptError
}
),
/* @__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" ? useNumericDimensions ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_image.default,
{
src: placeholder,
alt: altText,
width: numericWidth,
height: numericHeight,
style: { objectFit: "cover" },
priority: true
}
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_image.default,
{
src: placeholder,
alt: altText,
fill: true,
style: { objectFit: "cover" },
priority: true
}
) : 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 next_default = UnicornScene;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
UnicornScene
});
//# sourceMappingURL=next.js.map