├── components ├── unicornstudio-wix.js ├── unicornstudio-react.tsx ├── unicornstudio-solid.tsx ├── unicornstudio-figma.tsx └── unicornstudio-framer.js ├── README.md ├── extensions └── model-renderer.js └── dist └── unicornStudio.umd.js /components/unicornstudio-wix.js: -------------------------------------------------------------------------------- 1 | 2 | class UnicornStudioEmbed extends HTMLElement { 3 | constructor() { 4 | super(); 5 | this.attachShadow({ mode: "open" }); 6 | this.scene = null; // Initialize a property to store the scene reference 7 | } 8 | 9 | // Called when the element is added to the DOM 10 | connectedCallback() { 11 | this.initializeUnicornStudio(); 12 | } 13 | 14 | // Called when the element is removed from the DOM 15 | disconnectedCallback() { 16 | // If we have a scene reference, destroy it to clean up 17 | if (this.scene && typeof this.scene.destroy === "function") { 18 | this.scene.destroy(); 19 | } 20 | } 21 | 22 | // Ensure that the document head is available and then load the Unicorn Studio script 23 | loadUnicornStudioScript() { 24 | return new Promise((resolve, reject) => { 25 | const existingScript = document.querySelector( 26 | 'script[src="https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js"]' 27 | ); 28 | 29 | if (existingScript) { 30 | if (window.UnicornStudio) { 31 | resolve(); 32 | } else { 33 | existingScript.addEventListener("load", resolve); 34 | existingScript.addEventListener("error", reject); 35 | } 36 | } else { 37 | const appendScriptToHead = () => { 38 | const script = document.createElement("script"); 39 | script.src = "https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js@v1.5.3/dist/unicornStudio.umd.js"; 40 | script.onload = () => { 41 | resolve(); 42 | }; 43 | script.onerror = () => { 44 | console.error("Error loading Unicorn Studio script."); 45 | reject(); 46 | }; 47 | document.head.appendChild(script); // Append the script to the head 48 | }; 49 | 50 | // Check if the head exists or if it's not ready, wait for the document to load 51 | if (document.head) { 52 | appendScriptToHead(); 53 | } else { 54 | document.addEventListener("DOMContentLoaded", appendScriptToHead); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | // Initialize the Unicorn Studio scene inside the custom element 61 | initializeUnicornStudio() { 62 | this.loadUnicornStudioScript() 63 | .then(() => { 64 | if ( 65 | window.UnicornStudio && 66 | typeof window.UnicornStudio.addScene === "function" 67 | ) { 68 | const projectId = 69 | this.getAttribute("project-id") || "YOUR_PROJECT_EMBED_ID"; 70 | const dpi = this.getAttribute("dpi") || 1.5; 71 | const scale = this.getAttribute("scale") || 1; 72 | const lazyLoad = this.getAttribute("lazy-load") === "true"; 73 | const altText = 74 | this.getAttribute("alt-text") || "Welcome to Unicorn Studio"; 75 | const ariaLabel = 76 | this.getAttribute("aria-label") || "This is a canvas scene"; 77 | 78 | // Create a div container for the Unicorn scene and append it to the Shadow DOM 79 | const container = document.createElement("div"); 80 | container.classList.add("unicorn-embed"); 81 | container.style.width = "100%"; 82 | container.style.height = "100%"; 83 | this.shadowRoot.appendChild(container); 84 | 85 | // Directly pass the container element node as 'element' 86 | UnicornStudio.addScene({ 87 | element: container, // Pass the container DOM node directly using 'element' 88 | projectId, // Project ID from attribute 89 | dpi, // DPI setting 90 | scale, // Scale setting 91 | lazyLoad, // Lazy load option 92 | altText, // Alt text for SEO 93 | ariaLabel, // Aria label for accessibility 94 | }) 95 | .then((scene) => { 96 | this.scene = scene; // Store the scene reference for later cleanup 97 | }) 98 | .catch((err) => { 99 | console.error("Error loading Unicorn Studio scene:", err); 100 | }); 101 | } else { 102 | console.error( 103 | "Unicorn Studio is not available or addScene is not a function" 104 | ); 105 | } 106 | }) 107 | .catch((err) => { 108 | console.error("Error loading Unicorn Studio script:", err); 109 | }); 110 | } 111 | } 112 | 113 | // Define the custom element 114 | customElements.define("unicorn-studio-embed", UnicornStudioEmbed); 115 | -------------------------------------------------------------------------------- /components/unicornstudio-react.tsx: -------------------------------------------------------------------------------- 1 | // This is an example React component for Unicorn Studio 2 | // Use it for reference when building your own component 3 | 'use client'; 4 | 5 | import { useEffect, useRef, useState } from 'react'; 6 | 7 | export type UnicornSceneProps = { 8 | projectId?: string; 9 | jsonFilePath?: string; 10 | width?: number | string; 11 | height?: number | string; 12 | scale?: number; 13 | dpi?: number; 14 | fps?: number; 15 | altText?: string; 16 | ariaLabel?: string; 17 | className?: string; 18 | lazyLoad?: boolean; 19 | }; 20 | 21 | export default function UnicornScene({ 22 | projectId, 23 | jsonFilePath, 24 | width = "100%", 25 | height = "100%", 26 | scale = 1, 27 | dpi = 1.5, 28 | fps = 60, 29 | altText = "Unicorn Scene", 30 | ariaLabel = altText, 31 | className = "", 32 | lazyLoad = false, 33 | }: UnicornSceneProps) { 34 | const elementRef = useRef(null); 35 | const sceneRef = useRef<{ destroy: () => void } | null>(null); 36 | const [error, setError] = useState(null); 37 | const scriptId = useRef(`us-data-${Math.random().toString(36).slice(2)}`); 38 | 39 | useEffect(() => { 40 | if (typeof window === 'undefined') return; 41 | 42 | const initializeScript = (callback: () => void) => { 43 | const version = '1.5.2'; 44 | 45 | const existingScript = document.querySelector( 46 | 'script[src^="https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js"]' 47 | ); 48 | 49 | if (existingScript) { 50 | if ((window as Window & { UnicornStudio?: unknown }).UnicornStudio) { 51 | callback(); 52 | } else { 53 | existingScript.addEventListener('load', callback); 54 | } 55 | return; 56 | } 57 | 58 | const script = document.createElement('script'); 59 | script.src = `https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js@v${version}/dist/unicornStudio.umd.js`; 60 | script.async = true; 61 | 62 | script.onload = () => { 63 | callback(); 64 | }; 65 | script.onerror = () => setError('Failed to load UnicornStudio script'); 66 | 67 | document.body.appendChild(script); 68 | }; 69 | 70 | const initializeScene = async () => { 71 | if (!elementRef.current) return; 72 | 73 | if (jsonFilePath) { 74 | elementRef.current.setAttribute( 75 | "data-us-project-src", 76 | `${jsonFilePath}` 77 | ); 78 | } else if (projectId) { 79 | const [cleanProjectId, query] = projectId.split("?"); 80 | const production = query?.includes("production"); 81 | 82 | elementRef.current.setAttribute('data-us-project', cleanProjectId); 83 | 84 | if (production) { 85 | elementRef.current.setAttribute("data-us-production", "1"); 86 | } 87 | } else { 88 | throw new Error('No project ID or JSON file path provided'); 89 | } 90 | 91 | interface UnicornStudioType { 92 | init: (config: { scale: number; dpi: number }) => Promise void; 95 | contains?: (element: HTMLElement | null) => boolean; 96 | }>>; 97 | } 98 | 99 | const UnicornStudio = (window as Window & { UnicornStudio?: UnicornStudioType }).UnicornStudio; 100 | 101 | if (!UnicornStudio) { 102 | throw new Error('UnicornStudio not found'); 103 | } 104 | 105 | if (sceneRef.current?.destroy) { 106 | sceneRef.current.destroy(); 107 | } 108 | 109 | 110 | await UnicornStudio?.init({ 111 | scale, 112 | dpi, 113 | }).then((scenes) => { 114 | const ourScene = scenes.find( 115 | (scene) => 116 | scene.element === elementRef.current || 117 | scene.element.contains(elementRef.current) 118 | ); 119 | if (ourScene) { 120 | sceneRef.current = ourScene; 121 | } 122 | }); 123 | }; 124 | 125 | initializeScript(() => { 126 | void initializeScene(); 127 | }); 128 | 129 | return () => { 130 | if (sceneRef.current?.destroy) { 131 | sceneRef.current.destroy(); 132 | sceneRef.current = null; 133 | } 134 | if (jsonFilePath) { 135 | const script = document.getElementById(scriptId.current); 136 | script?.remove(); 137 | } 138 | }; 139 | }, [projectId, jsonFilePath, scale, dpi]); 140 | 141 | return ( 142 |
158 | {error &&
{error}
} 159 |
160 | ); 161 | } -------------------------------------------------------------------------------- /components/unicornstudio-solid.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect, onCleanup, createSignal, Component } from "solid-js"; 2 | 3 | // Augment the window object to include UnicornStudio for TypeScript 4 | declare global { 5 | interface Window { 6 | UnicornStudio?: { 7 | init: (config: { scale: number; dpi: number }) => Promise void; 10 | contains?: (element: HTMLElement | null) => boolean; 11 | }>>; 12 | }; 13 | } 14 | } 15 | 16 | // Define the props for the component, matching the React version 17 | export interface UnicornSceneProps { 18 | projectId?: string; 19 | jsonFilePath?: string; 20 | width?: number | string; 21 | height?: number | string; 22 | scale?: number; 23 | dpi?: number; 24 | fps?: number; 25 | altText?: string; 26 | ariaLabel?: string; 27 | className?: string; 28 | lazyLoad?: boolean; 29 | } 30 | 31 | const UnicornScene: Component = (props) => { 32 | // Set default values for props 33 | const p = { 34 | width: "100%", 35 | height: "100%", 36 | scale: 1, 37 | dpi: 1.5, 38 | fps: 60, 39 | altText: "Unicorn Scene", 40 | get ariaLabel() { return this.altText }, 41 | className: "", 42 | lazyLoad: false, 43 | ...props 44 | }; 45 | 46 | let elementRef: HTMLDivElement | undefined; 47 | let sceneRef: { destroy: () => void } | null = null; 48 | const [error, setError] = createSignal(null); 49 | 50 | createEffect(() => { 51 | // Re-run this effect if these properties change 52 | const { projectId, jsonFilePath, scale, dpi } = p; 53 | 54 | if (typeof window === 'undefined') return; 55 | 56 | const initializeScript = (callback: () => void) => { 57 | const version = '1.5.2'; 58 | const scriptSrc = `https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js@v${version}/dist/unicornStudio.umd.js`; 59 | const existingScript = document.querySelector(`script[src="${scriptSrc}"]`); 60 | 61 | if (existingScript) { 62 | if (window.UnicornStudio) { 63 | callback(); 64 | } else { 65 | existingScript.addEventListener('load', callback); 66 | } 67 | return; 68 | } 69 | 70 | const script = document.createElement('script'); 71 | script.src = scriptSrc; 72 | script.async = true; 73 | script.onload = callback; 74 | script.onerror = () => setError('Failed to load UnicornStudio script'); 75 | document.body.appendChild(script); 76 | }; 77 | 78 | const initializeScene = async () => { 79 | if (!elementRef) return; 80 | 81 | if (jsonFilePath) { 82 | elementRef.setAttribute("data-us-project-src", jsonFilePath); 83 | } else if (projectId) { 84 | const [cleanProjectId, query] = projectId.split("?"); 85 | const production = query?.includes("production"); 86 | elementRef.setAttribute('data-us-project', cleanProjectId); 87 | if (production) { 88 | elementRef.setAttribute("data-us-production", "1"); 89 | } 90 | } else { 91 | setError('No project ID or JSON file path provided'); 92 | return; 93 | } 94 | 95 | const UnicornStudio = window.UnicornStudio; 96 | if (!UnicornStudio) { 97 | setError('UnicornStudio library not found on window object.'); 98 | return; 99 | } 100 | 101 | if (sceneRef?.destroy) { 102 | sceneRef.destroy(); 103 | } 104 | 105 | try { 106 | const scenes = await UnicornStudio.init({ scale, dpi }); 107 | const ourScene = scenes.find( 108 | (scene) => 109 | scene.element === elementRef || 110 | scene.element.contains?.(elementRef) 111 | ); 112 | if (ourScene) { 113 | sceneRef = ourScene; 114 | } 115 | } catch (e) { 116 | console.error("Failed to initialize Unicorn Studio scene:", e); 117 | setError("Failed to initialize scene."); 118 | } 119 | }; 120 | 121 | initializeScript(() => { 122 | void initializeScene(); 123 | }); 124 | 125 | onCleanup(() => { 126 | if (sceneRef?.destroy) { 127 | sceneRef.destroy(); 128 | sceneRef = null; 129 | } 130 | }); 131 | }); 132 | 133 | return ( 134 | 152 | ); 153 | }; 154 | 155 | export default UnicornScene; 156 | -------------------------------------------------------------------------------- /components/unicornstudio-figma.tsx: -------------------------------------------------------------------------------- 1 | import { defineProperties } from "figma:react" 2 | 3 | "use client" 4 | 5 | import { useCallback, useEffect, useRef, useState } from "react" 6 | 7 | export type UnicornSceneProps = { 8 | projectId?: string 9 | jsonFilePath?: string 10 | projectJSON?: string 11 | sdkVersion?: string 12 | width?: number | string 13 | height?: number | string 14 | scale?: number 15 | dpi?: number 16 | fps?: number 17 | altText?: string 18 | ariaLabel?: string 19 | className?: string 20 | lazyLoad?: boolean 21 | fixed?: boolean 22 | header?: string 23 | } 24 | 25 | export default function UnicornScene({ 26 | projectId, 27 | jsonFilePath, 28 | projectJSON, 29 | sdkVersion = "1.5.2", 30 | width = "100%", 31 | height = "100%", 32 | scale = 1, 33 | dpi = 1.5, 34 | fps = 60, 35 | altText = "Unicorn Scene", 36 | ariaLabel = altText, 37 | className = "", 38 | lazyLoad = false, 39 | fixed = false, 40 | header, 41 | }: UnicornSceneProps) { 42 | const elementRef = useRef(null) 43 | const sceneRef = useRef<{ destroy: () => void } | null>(null) 44 | const [error, setError] = useState(null) 45 | const scriptId = useRef(`us-data-${Math.random().toString(36).slice(2)}`) 46 | const setElementRef = useCallback((node: HTMLDivElement | null) => { 47 | elementRef.current = node 48 | }, []) 49 | 50 | const isValidVersion = (v: string) => /^\d+\.\d+\.\d+(-beta)?$/.test(v.trim()) 51 | 52 | useEffect(() => { 53 | if (typeof window === "undefined") return 54 | 55 | if (!isValidVersion(sdkVersion)) { 56 | setError( 57 | `Invalid SDK version "${sdkVersion}". Use x.y.z (e.g. 1.4.34).` 58 | ) 59 | return 60 | } else { 61 | setError(null) 62 | } 63 | 64 | const initializeScript = (callback: () => void) => { 65 | const cdnPrefix = 66 | "https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js" 67 | const scriptSrc = `${cdnPrefix}@v${sdkVersion}/dist/unicornStudio.umd.js` 68 | 69 | const existingScript = document.querySelector( 70 | 'script[src^="https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js"]' 71 | ) as HTMLScriptElement | null 72 | 73 | if (!existingScript) { 74 | const script = document.createElement("script") 75 | script.src = scriptSrc 76 | script.async = true 77 | script.onload = callback 78 | script.onerror = () => 79 | setError(`Failed to load UnicornStudio script at ${scriptSrc}`) 80 | document.body.appendChild(script) 81 | } else { 82 | if ((window as any).UnicornStudio) { 83 | callback() 84 | } else { 85 | const waitForLoad = setInterval(() => { 86 | if ((window as any).UnicornStudio) { 87 | clearInterval(waitForLoad) 88 | callback() 89 | } 90 | }, 100) 91 | } 92 | } 93 | } 94 | 95 | const initializeScene = () => { 96 | if (!elementRef.current) return 97 | 98 | // Clean up any previous scene on this element 99 | if (sceneRef.current?.destroy) { 100 | sceneRef.current.destroy() 101 | sceneRef.current = null 102 | } 103 | 104 | if (projectJSON) { 105 | // ensure we don't leak old inline data scripts on prop change 106 | const previous = document.getElementById(scriptId.current) 107 | previous?.remove() 108 | 109 | const dataScript = document.createElement("script") 110 | dataScript.id = scriptId.current 111 | dataScript.type = "application/json" 112 | dataScript.textContent = projectJSON 113 | document.body.appendChild(dataScript) 114 | elementRef.current.setAttribute("data-us-project-src", scriptId.current) 115 | } else if (jsonFilePath) { 116 | elementRef.current.setAttribute("data-us-project-src", `${jsonFilePath}`) 117 | } else if (projectId) { 118 | const [cleanProjectId, query] = projectId.split("?") 119 | const production = query?.includes("production") 120 | elementRef.current.setAttribute("data-us-project", cleanProjectId) 121 | if (production) { 122 | elementRef.current.setAttribute("data-us-production", "1") 123 | } 124 | } else { 125 | setError("No project ID or JSON provided") 126 | return 127 | } 128 | 129 | const US = (window as any).UnicornStudio 130 | if (!US) { 131 | setError("UnicornStudio not found") 132 | return 133 | } 134 | 135 | const args: any = { 136 | element: elementRef.current, 137 | dpi, 138 | scale, 139 | fps, 140 | lazyLoad: !!lazyLoad, 141 | fixed: !!fixed, 142 | altText, 143 | ariaLabel, 144 | } 145 | 146 | if (projectJSON) { 147 | args.filePath = scriptId.current 148 | } else if (jsonFilePath) { 149 | args.filePath = jsonFilePath 150 | } else if (projectId) { 151 | args.projectId = projectId 152 | } 153 | 154 | US.addScene(args) 155 | .then((scene: any) => { 156 | sceneRef.current = scene 157 | }) 158 | .catch(() => { 159 | setError("Failed to initialize UnicornStudio scene") 160 | }) 161 | } 162 | 163 | const start = () => initializeScene() 164 | 165 | if ((window as any).UnicornStudio) { 166 | start() 167 | } else { 168 | initializeScript(start) 169 | } 170 | 171 | return () => { 172 | if (sceneRef.current?.destroy) { 173 | sceneRef.current.destroy() 174 | sceneRef.current = null 175 | } 176 | 177 | const dataScript = document.getElementById(scriptId.current) 178 | if (dataScript) { 179 | dataScript.remove() 180 | } 181 | } 182 | }, [ 183 | projectId, 184 | jsonFilePath, 185 | projectJSON, 186 | sdkVersion, 187 | scale, 188 | dpi, 189 | fps, 190 | lazyLoad, 191 | fixed, 192 | altText, 193 | ariaLabel, 194 | ]) 195 | 196 | return ( 197 |
213 | {header ? ( 214 |

225 | {header} 226 |

227 | ) : null} 228 | {error &&
{error}
} 229 |
230 | ) 231 | } 232 | 233 | defineProperties(UnicornScene, { 234 | projectId: { 235 | label: "Project Id", 236 | type: "string", 237 | defaultValue: "Add your project Id", 238 | }, 239 | projectJSON: { 240 | label: "Project JSON", 241 | type: "string", 242 | defaultValue: "", 243 | }, 244 | jsonFilePath: { 245 | label: "JSON File Path", 246 | type: "string", 247 | defaultValue: "", 248 | }, 249 | sdkVersion: { 250 | label: "SDK Version", 251 | type: "string", 252 | defaultValue: "1.5.2", 253 | }, 254 | scale: { 255 | type: "number", 256 | label: "Scale", 257 | defaultValue: 1, 258 | }, 259 | dpi: { 260 | type: "number", 261 | label: "DPI", 262 | defaultValue: 1.5, 263 | }, 264 | fps: { 265 | type: "number", 266 | label: "FPS", 267 | defaultValue: 60, 268 | }, 269 | altText: { 270 | type: "string", 271 | label: "Alt text", 272 | defaultValue: "Unicorn Scene", 273 | }, 274 | ariaLabel: { 275 | type: "string", 276 | label: "Aria label", 277 | defaultValue: "Unicorn Scene", 278 | }, 279 | lazyLoad: { 280 | type: "boolean", 281 | label: "Lazy Load", 282 | defaultValue: false, 283 | }, 284 | fixed: { 285 | type: "boolean", 286 | label: "Fixed", 287 | defaultValue: false, 288 | }, 289 | header: { 290 | type: "string", 291 | label: "H1 text", 292 | defaultValue: "", 293 | }, 294 | }) 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embed your Unicorn Studio projects 2 | 3 | ## Include the script 4 | 5 | Add the script tag to the `` of your page 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | or import into your component 12 | 13 | ```js 14 | import * as UnicornStudio from "./path/to/unicornStudio.umd.js"; 15 | ``` 16 | 17 | ## Initialize your scene: 18 | 19 | ### Inline 20 | 21 | Any element with `data-us-project` will get initialized by calling `UnicornStudio.init()`. If you're hosting your own exported JSON file, use `data-us-project-src` to point to its location. You do not need both `data-us-project` and `data-us-project-src`. If you host your own JSON, remembder you'll need to update this file when you make changes to your scene in Unicorn.studio. 22 | 23 | ```html 24 |
34 | 43 | ``` 44 | 45 | ### Dynamically 46 | 47 | You can add a scene dynamically during or after pageload. 48 | 49 | ```html 50 |
51 | 90 | ``` 91 | 92 | Any values set in the UI will be overridden by values defined in the optional params. 93 | 94 | ## Destroy all scenes: 95 | 96 | If you're using UnicornStudio in a SPA with dynamic routing, make sure to destroy them on unmount. 97 | 98 | ```js 99 | UnicornStudio.destroy(); 100 | ``` 101 | 102 | ## React/Next 103 | See [this repo](https://github.com/diegopeixoto/unicornstudio-react) for a great react/next npm package. 104 | 105 | ## Live example 106 | 107 | https://codepen.io/georgehastings/pen/ExGrqMJ 108 | 109 | 110 | # Changelog 111 | v1.5.3 112 | - Adds support for mask layer depth 113 | 114 | v1.5.2 115 | - Introduces the Model class! 116 | - Bugfixes and optimizations 117 | 118 | v1.4.36 119 | - Fixes a bug that caused unexpected resizing when the canvas is affected by CSS transforms 120 | - Fixes a bug that didn't allow appear animations to complete to the final frame 121 | 122 | v1.4.35 123 | - Fixes a stretching bug caused by certain effects when the background layer is hidden 124 | 125 | v1.4.34 126 | - Events work for video playback speed now 127 | - No longer rounds element position to whole pixel numbers 128 | 129 | v1.4.33 130 | - Fixes positioning bug with track mouse x/y controls 131 | - Stability improvements 132 | 133 | v1.4.32 134 | - Cancels raf properly when all scenes are destroyed 135 | - Adds support for disabling text-as-html 136 | 137 | v1.4.31 138 | - Fixes bug when background layer is hidden 139 | - Removes curtains log 140 | 141 | v1.4.30 142 | - Adds mouse axis control 143 | - Framework for additional easing functions 144 | - Minor performance optimizations 145 | 146 | v1.4.29 147 | - Adds control for video looping 148 | - Adds control for video playback speed 149 | 150 | v1.4.28 151 | - Adds looping functionality for appear events 152 | - Adds a disable parameter `data-us-disablemouse` for mouse interactivity that can be used dynamically 153 | - Fixes a bug where 0 speed effects would still animate 154 | 155 | v1.4.27 156 | - Fixes a bug where appear effects on Element layers wouldn't properly initialize 157 | 158 | v1.4.27 159 | - Fixes a bug where appear effects on Element layers wouldn't properly initialize 160 | 161 | v1.4.26 162 | - Fixes a bug with texture handling of shared textures 163 | - Eliminated cases of redundant texture loading 164 | - Fixed a bug handling fontCSS.src being undefined 165 | 166 | v1.4.25 167 | - Enables momentum for mouse trail effects (light, mouse, ripple) 168 | - More mouse perf optimizations 169 | 170 | v1.4.24 171 | - Fixes a bug with texture handling for multipass child effects 172 | 173 | v1.4.23 174 | - Fixes a bug where mouse tracking would get offset when moved while scrolling 175 | - Adds "fixed" param to declaratively make a scene fixed with data-us-fixed 176 | 177 | v1.4.22 178 | - Child effect rendertarget bugfix 179 | - Improved caching and mouse tracking performance 180 | 181 | v1.4.21 182 | - Continues text rendering even if font fails to load 183 | - Fixes bug with effects that use canvas as uBgTexture twice 184 | - Fixes bug where rotation animations didnt work for text elements 185 | - Adds the "scene.paused" parameter to pause the scene 186 | - Minor optimizations 187 | 188 | v1.4.20 189 | - Fixes texture stretch bug when background is hidden and an element is the first layer 190 | 191 | v1.4.19 192 | - Much more efficient downsampling logic in plane initialization 193 | - Created vertices by accident for noise and sine effects 194 | - No longer creates a shader program for hidden background layers 195 | 196 | v1.4.18 197 | - Fixed a bug with handling multi-pass child effects like water ripple 198 | - Minor optimizations 199 | 200 | v1.4.17 201 | - Removed a code check that automatically throttled scene quality by checking for low end devices or GPUs. At best it likely had little positive impact and at worst it thew false positives in certain browsers and high end mobile devices. 202 | - Fixed a bug that broke user downsapling for element layers 203 | 204 | v1.4.14 205 | - Fixes an image texture handling bug 206 | 207 | v1.4.13 208 | - Fixes a downsampling bug from the previous release 209 | 210 | v1.4.12 211 | - Corrected unecessary texture creation in multipass planes 212 | - Fixed a bug that prevented planes from being downsampled correctly 213 | - Fixed a bug that didn't remove text when using scene.destroy() 214 | - Fixed a texture loading race condition bug 215 | 216 | v1.4.11 217 | - Fixed a texture handling bug when using multiple effects with blue noise 218 | - Fixed some inconsistency with font weights 219 | 220 | v1.4.10 221 | - Added the changelog to the README 222 | - Fixed a texture bug with mouse effects as child effects 223 | 224 | v.1.4.9 225 | - scene.destroy() now removes canvas element as this is the expected behavior 226 | - fixes a bug where some element properties were not responsive to breakpoints 227 | - fixes a bug where some effect properties did not respond to events 228 | - fixes some bugs with appear events when scenes are scroll away / tabbed away mid animation 229 | 230 | v1.4.8 231 | - Fixed a bug when resizing mouse trail effects 232 | - Fixed a bug where videos would pause after the tab was inactive for awhile 233 | - Other minor bugfixes 234 | 235 | v1.4.7 236 | - Added breakpoint control for events 237 | 238 | v1.4.6 239 | - Fixed a bug that prevented effects from getting responsiveness updates after initial pageload 240 | - Fixed a bug where isFixed scenes mouse tracking was wrong 241 | 242 | v1.4.5 243 | - Added flexibility for future pingpong effects 244 | - Bugfix for scene.destroy() 245 | 246 | v1.4.4 247 | - Scene resizing is now way more responsive and way less memory intensive 248 | - New texture preloading logic for faster load times 249 | - Overall performance enhancements 250 | 251 | v1.4.3 252 | - Scene resizing is now way more responsive and way less memory intensive 253 | - New texture preloading logic for faster load times 254 | - Overall performance enhancements 255 | 256 | v.1.4.2 257 | - Handles hover events for hovering over the elements themselves 258 | - Performance enhancements 259 | 260 | v1.4.1 261 | - This release adds events to all element layers (Images/Shapes/Text) 262 | - Adds support for handling videos 263 | - Stability improvements 264 | 265 | ## License 266 | 267 | Copyright © 2025 Unicorn Studio (UNCRN LLC) 268 | 269 | Permission is granted to use this software only for integration with legitimate Unicorn Studio projects. The source code is made available for transparency and to facilitate integration, but remains proprietary. 270 | 271 | Unauthorized uses include but are not limited to: 272 | 1. Using this software in any way that violates Unicorn Studio's Terms of Service 273 | 2. Creating derivative works not approved by Unicorn Studio 274 | 3. Using this software with non-Unicorn Studio projects 275 | 4. Reverse engineering this software to recreate Unicorn Studio functionality 276 | 5. Removing or altering any license, copyright, watermark, or other proprietary notices 277 | 278 | This license is subject to termination if violated. All rights not explicitly granted are reserved. 279 | -------------------------------------------------------------------------------- /components/unicornstudio-framer.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react" 2 | import { addPropertyControls, ControlType, RenderTarget } from "framer" 3 | 4 | /** 5 | * @framerSupportedLayoutWidth fixed 6 | * @framerSupportedLayoutHeight fixed 7 | * @framerIntrinsicHeight 400 8 | * @framerIntrinsicWidth 800 9 | */ 10 | export default function UnicornStudioEmbed(props) { 11 | const { 12 | sdkVersion = "1.5.3", // default 13 | } = props 14 | 15 | const elementRef = useRef(null) 16 | const sceneRef = useRef(null) 17 | const scriptId = useRef( 18 | `unicorn-project-${Math.random().toString(36).substr(2, 9)}` 19 | ) 20 | const [versionError, setVersionError] = useState(null) 21 | 22 | // simple semver check: 1.2.3 23 | const isValidVersion = (v: string) => 24 | /^\d+\.\d+\.\d+(-beta)?$/.test(v.trim()) 25 | 26 | useEffect(() => { 27 | const isEditingOrPreviewing = ["CANVAS", "PREVIEW"].includes( 28 | RenderTarget.current() 29 | ) 30 | 31 | // validate version first 32 | if (!isValidVersion(sdkVersion)) { 33 | console.error( 34 | `[UnicornStudioEmbed] Invalid SDK version "${sdkVersion}". Expected format: x.y.z (e.g. 1.4.34)` 35 | ) 36 | setVersionError( 37 | `Invalid SDK version "${sdkVersion}". Use x.y.z (e.g. 1.4.34).` 38 | ) 39 | return 40 | } else { 41 | setVersionError(null) 42 | } 43 | 44 | if (RenderTarget.current() === "CANVAS") { 45 | return 46 | } 47 | 48 | const initializeScript = (callback: () => void) => { 49 | const cdnPrefix = 50 | "https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js" 51 | const scriptSrc = `${cdnPrefix}@v${sdkVersion}/dist/unicornStudio.umd.js` 52 | 53 | // check if any unicornstudio.js script is already on the page 54 | const existingScript = document.querySelector( 55 | 'script[src^="https://cdn.jsdelivr.net/gh/hiunicornstudio/unicornstudio.js"]' 56 | ) as HTMLScriptElement | null 57 | 58 | // IMPORTANT: 59 | // If there is already a script but it's for a different version, 60 | // we might want to load the desired one anyway. For now, if it's 61 | // present AND window.UnicornStudio exists, we just use it. 62 | if (!existingScript) { 63 | const script = document.createElement("script") 64 | script.src = scriptSrc 65 | script.onload = callback 66 | script.onerror = () => 67 | console.error( 68 | "Failed to load UnicornStudio script at " + scriptSrc 69 | ) 70 | document.body.appendChild(script) 71 | } else { 72 | if ((window as any).UnicornStudio) { 73 | callback() 74 | } else { 75 | const waitForLoad = setInterval(() => { 76 | if ((window as any).UnicornStudio) { 77 | clearInterval(waitForLoad) 78 | callback() 79 | } 80 | }, 100) 81 | } 82 | } 83 | } 84 | 85 | const initializeUnicornStudio = () => { 86 | if (props.projectJSON) { 87 | try { 88 | // Create script element for JSON data 89 | const dataScript = document.createElement("script") 90 | dataScript.id = scriptId.current 91 | dataScript.type = "application/json" 92 | dataScript.textContent = props.projectJSON 93 | document.body.appendChild(dataScript) 94 | 95 | elementRef.current?.setAttribute( 96 | "data-us-project-src", 97 | `${scriptId.current}` 98 | ) 99 | } catch (e) { 100 | console.error("Failed to parse project JSON:", e) 101 | return 102 | } 103 | } else if (props.projectId) { 104 | const query = props.projectId.split("?") 105 | const projectId = query[0] 106 | const production = query[1] && query[1].includes("production") 107 | const cacheBuster = isEditingOrPreviewing 108 | ? "?update=" + Math.random() 109 | : "" 110 | elementRef.current?.setAttribute( 111 | "data-us-project", 112 | projectId + cacheBuster 113 | ) 114 | 115 | if (production) { 116 | elementRef.current?.setAttribute("data-us-production", "1") 117 | } 118 | } 119 | 120 | const US = (window as any).UnicornStudio 121 | if (!US || !elementRef.current) return 122 | 123 | // 1) Clean up only our previous scene 124 | US.scenes?.find((s) => s.element === elementRef.current)?.destroy() 125 | 126 | // 2) Prepare args 127 | const args: any = { 128 | element: elementRef.current, 129 | dpi: props.dpi, 130 | scale: props.scale, 131 | fps: props.fps, 132 | lazyLoad: !!props.lazyLoad, 133 | fixed: !!props.fixed, 134 | altText: props.altText, 135 | ariaLabel: props.ariaLabel, 136 | } 137 | 138 | // If you passed JSON via a