diff --git a/.changeset/sweet-comics-confess.md b/.changeset/sweet-comics-confess.md new file mode 100644 index 00000000..fc62880b --- /dev/null +++ b/.changeset/sweet-comics-confess.md @@ -0,0 +1,6 @@ +--- +'@lottiefiles/dotlottie-web': minor +--- + +fix: 🐛 loading wasm binaries +feat:🎸 add setWasmUrl static method diff --git a/apps/dotlottie-web-example/src/main.ts b/apps/dotlottie-web-example/src/main.ts index bf354e77..c3043832 100644 --- a/apps/dotlottie-web-example/src/main.ts +++ b/apps/dotlottie-web-example/src/main.ts @@ -5,6 +5,8 @@ import './styles.css'; import { DotLottie } from '@lottiefiles/dotlottie-web'; +import wasmUrl from '../../../packages/web/dist/renderer.wasm?url'; + const app = document.getElementById('app') as HTMLDivElement; app.innerHTML = ` @@ -23,6 +25,8 @@ app.innerHTML = ` `; +DotLottie.setWasmUrl(wasmUrl); + fetch('/hamster.lottie') .then(async (res) => res.arrayBuffer()) .then((data): void => { diff --git a/packages/web/README.md b/packages/web/README.md index 5ac57b5f..3ac24dd2 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -24,6 +24,7 @@ * [Options](#options) * [Properties](#properties) * [Methods](#methods) + * [Static Methods](#static-methods) * [Events](#events) * [Development](#development) * [Setup](#setup) @@ -142,6 +143,12 @@ const dotLottie = new DotLottie({ | `addEventListener(event: string, listener: Function)` | Registers a function to respond to a specific animation event. | | `removeEventListener(event: string, listener?: Function)` | Removes a previously registered function from responding to a specific animation event. | +### Static Methods + +| Method | Description | +| ------------------------- | ----------------------------------------- | +| `setWasmUrl(url: string)` | Sets the URL to the renderer.wasm binary. | + ### Events | Event | Description | diff --git a/packages/web/src/dotlottie.ts b/packages/web/src/dotlottie.ts index d7f76159..f00ae941 100644 --- a/packages/web/src/dotlottie.ts +++ b/packages/web/src/dotlottie.ts @@ -9,7 +9,7 @@ import type { EventListener, EventType } from './event-manager'; import { EventManager } from './event-manager'; import type { Renderer } from './renderer-wasm'; -import { createRenderer } from './renderer-wasm'; +import { WasmLoader } from './renderer-wasm'; import { getAnimationJSONFromDotLottie, loadAnimationJSONFromURL } from './utils'; const MS_TO_SEC_FACTOR = 1000; @@ -79,8 +79,10 @@ export class DotLottie { this._speed = config.speed ?? 1; this._autoplay = config.autoplay ?? false; - this._initRenderer() - .then(() => { + WasmLoader.awaitInstance() + .then((renderer) => { + this._renderer = renderer; + if (config.src) { this._loadAnimationFromURL(config.src); } else if (config.data) { @@ -160,17 +162,6 @@ export class DotLottie { } // #endregion - // #region Private Methods - /** - * Initializes the renderer. - * - * @returns A promise that resolves when the renderer is initialized. - */ - private async _initRenderer(): Promise { - if (this._renderer) return; - this._renderer = await createRenderer(); - } - /** * Loads and initializes the animation from a given URL. * @@ -433,5 +424,13 @@ export class DotLottie { this._eventManager.removeEventListener(type, listener); } + /** + * Sets the source URL of the WASM file to load. + * @param url - The URL of the WASM file to load. + */ + public static setWasmUrl(url: string): void { + WasmLoader.setWasmUrl(url); + } + // #endregion } diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 5be2cb5d..42f4ea54 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -5,3 +5,4 @@ export * from './dotlottie'; export * from './event-manager'; export * from './utils'; +export * from './renderer-wasm'; diff --git a/packages/web/src/renderer-wasm/bin/renderer.js b/packages/web/src/renderer-wasm/bin/renderer.js index 1fc9d942..e7fda017 100644 --- a/packages/web/src/renderer-wasm/bin/renderer.js +++ b/packages/web/src/renderer-wasm/bin/renderer.js @@ -1,5 +1,5 @@ var createRendererModule = (() => { - var _scriptDir = import.meta.url; + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; return function (moduleArg = {}) { var Module = moduleArg; @@ -238,13 +238,10 @@ var createRendererModule = (() => { var wasmBinaryFile; - if (Module['locateFile']) { - wasmBinaryFile = 'renderer.wasm'; - if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); - } - } else { - wasmBinaryFile = new URL('renderer.wasm', import.meta.url).href; + wasmBinaryFile = 'renderer.wasm'; + + if (!isDataURI(wasmBinaryFile)) { + wasmBinaryFile = locateFile(wasmBinaryFile); } function getBinarySync(file) { @@ -1360,26 +1357,6 @@ var createRendererModule = (() => { } }; - function newFunc(constructor, argumentList) { - if (!(constructor instanceof Function)) { - throw new TypeError(`new_ called with constructor type ${typeof constructor} which is not a function`); - } - /* - * Previously, the following line was just: - * function dummy() {}; - * Unfortunately, Chrome was preserving 'dummy' as the object's name, even - * though at creation, the 'dummy' has the correct constructor name. Thus, - * objects created with IMVU.new would show up in the debugger as 'dummy', - * which isn't very helpful. Using IMVU.createNamedFunction addresses the - * issue. Doublely-unfortunately, there's no way to write a test for this - * behavior. -NRD 2013.02.22 - */ var dummy = createNamedFunction(constructor.name || 'unknownFunctionName', function () {}); - dummy.prototype = constructor.prototype; - var obj = new dummy(); - var r = constructor.apply(obj, argumentList); - return r instanceof Object ? r : obj; - } - function craftInvokerFunction( humanName, argTypes, @@ -1401,72 +1378,46 @@ var createRendererModule = (() => { } } var returns = argTypes[0].name !== 'void'; - var argsList = ''; - var argsListWired = ''; - for (var i = 0; i < argCount - 2; ++i) { - argsList += (i !== 0 ? ', ' : '') + 'arg' + i; - argsListWired += (i !== 0 ? ', ' : '') + 'arg' + i + 'Wired'; - } - var invokerFnBody = `\n return function ${makeLegalFunctionName( - humanName, - )}(${argsList}) {\n if (arguments.length !== ${ - argCount - 2 - }) {\n throwBindingError('function ${humanName} called with ' + arguments.length + ' arguments, expected ${ - argCount - 2 - }');\n }`; - if (needsDestructorStack) { - invokerFnBody += 'var destructors = [];\n'; - } - var dtorStack = needsDestructorStack ? 'destructors' : 'null'; - var args1 = ['throwBindingError', 'invoker', 'fn', 'runDestructors', 'retType', 'classParam']; - var args2 = [throwBindingError, cppInvokerFunc, cppTargetFunc, runDestructors, argTypes[0], argTypes[1]]; - if (isClassMethodFunc) { - invokerFnBody += 'var thisWired = classParam.toWireType(' + dtorStack + ', this);\n'; - } - for (var i = 0; i < argCount - 2; ++i) { - invokerFnBody += - 'var arg' + - i + - 'Wired = argType' + - i + - '.toWireType(' + - dtorStack + - ', arg' + - i + - '); // ' + - argTypes[i + 2].name + - '\n'; - args1.push('argType' + i); - args2.push(argTypes[i + 2]); - } - if (isClassMethodFunc) { - argsListWired = 'thisWired' + (argsListWired.length > 0 ? ', ' : '') + argsListWired; - } - invokerFnBody += - (returns || isAsync ? 'var rv = ' : '') + - 'invoker(fn' + - (argsListWired.length > 0 ? ', ' : '') + - argsListWired + - ');\n'; - if (needsDestructorStack) { - invokerFnBody += 'runDestructors(destructors);\n'; - } else { - for (var i = isClassMethodFunc ? 1 : 2; i < argTypes.length; ++i) { - var paramName = i === 1 ? 'thisWired' : 'arg' + (i - 2) + 'Wired'; - if (argTypes[i].destructorFunction !== null) { - invokerFnBody += paramName + '_dtor(' + paramName + '); // ' + argTypes[i].name + '\n'; - args1.push(paramName + '_dtor'); - args2.push(argTypes[i].destructorFunction); + var expectedArgCount = argCount - 2; + var argsWired = new Array(expectedArgCount); + var invokerFuncArgs = []; + var destructors = []; + return function () { + if (arguments.length !== expectedArgCount) { + throwBindingError( + `function ${humanName} called with ${arguments.length} arguments, expected ${expectedArgCount}`, + ); + } + destructors.length = 0; + var thisWired; + invokerFuncArgs.length = isClassMethodFunc ? 2 : 1; + invokerFuncArgs[0] = cppTargetFunc; + if (isClassMethodFunc) { + thisWired = argTypes[1]['toWireType'](destructors, this); + invokerFuncArgs[1] = thisWired; + } + for (var i = 0; i < expectedArgCount; ++i) { + argsWired[i] = argTypes[i + 2]['toWireType'](destructors, arguments[i]); + invokerFuncArgs.push(argsWired[i]); + } + var rv = cppInvokerFunc.apply(null, invokerFuncArgs); + function onDone(rv) { + if (needsDestructorStack) { + runDestructors(destructors); + } else { + for (var i = isClassMethodFunc ? 1 : 2; i < argTypes.length; i++) { + var param = i === 1 ? thisWired : argsWired[i - 2]; + if (argTypes[i].destructorFunction !== null) { + argTypes[i].destructorFunction(param); + } + } + } + if (returns) { + return argTypes[0]['fromWireType'](rv); } } - } - if (returns) { - invokerFnBody += 'var ret = retType.fromWireType(rv);\n' + 'return ret;\n'; - } else { - } - invokerFnBody += '}\n'; - args1.push(invokerFnBody); - return newFunc(Function, args1).apply(null, args2); + return onDone(rv); + }; } var __embind_register_class_constructor = ( diff --git a/packages/web/src/renderer-wasm/index.ts b/packages/web/src/renderer-wasm/index.ts index 5e450d0f..4bca441f 100644 --- a/packages/web/src/renderer-wasm/index.ts +++ b/packages/web/src/renderer-wasm/index.ts @@ -2,6 +2,11 @@ * Copyright 2023 Design Barn Inc. */ +/* eslint-disable no-negated-condition */ +/* eslint-disable no-console */ + +import pkg from '../../package.json'; + import createRendererModule from './bin/renderer'; export interface Renderer { @@ -16,10 +21,61 @@ export interface Renderer { update(): boolean; } -export async function createRenderer(): Promise { - const rendererModule = await createRendererModule(); +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class WasmLoader { + private static _renderer: Renderer | null = null; + + private static _isLoading = false; + + private static _wasmURL = `https://unpkg.com/${pkg.name}@${pkg.version}/dist/renderer.wasm`; + + private constructor() { + // Class is never instantiated + } + + public static loadRenderer(): void { + createRendererModule({ + locateFile: () => WasmLoader._wasmURL, + }) + .then((module: { Renderer: new () => Renderer }) => { + WasmLoader._renderer = new module.Renderer(); + }) + .catch(() => { + const backupJsdelivrUrl = `https://cdn.jsdelivr.net/npm/${pkg.name}@${pkg.version}/dist/renderer.wasm`; + + if (WasmLoader._wasmURL.toLowerCase() !== backupJsdelivrUrl) { + console.warn(`Failed to load WASM from ${WasmLoader._wasmURL}, trying jsdelivr as a backup`); + WasmLoader.setWasmUrl(backupJsdelivrUrl); + WasmLoader.loadRenderer(); + } else { + console.error( + `Could not load Rive WASM file from unpkg or jsdelivr, network connection may be down, or \ + you may need to call set a new WASM source via WasmLoader.setWasmUrl() and call \ + WasmLoader.loadRenderer() again`, + ); + } + }); + } + + public static getInstance(callback: (renderer: Renderer) => void): void { + if (!WasmLoader._isLoading) { + WasmLoader._isLoading = true; + WasmLoader.loadRenderer(); + } + + if (WasmLoader._renderer) { + // eslint-disable-next-line node/callback-return + callback(WasmLoader._renderer); + } else { + setTimeout(() => WasmLoader.getInstance(callback), 100); + } + } - const renderer = new rendererModule.Renderer(); + public static async awaitInstance(): Promise { + return new Promise((resolve) => WasmLoader.getInstance((renderer: Renderer): void => resolve(renderer))); + } - return renderer; + public static setWasmUrl(url: string): void { + WasmLoader._wasmURL = url; + } } diff --git a/packages/web/tsconfig.build.json b/packages/web/tsconfig.build.json index bcfcae7b..d2221362 100644 --- a/packages/web/tsconfig.build.json +++ b/packages/web/tsconfig.build.json @@ -8,7 +8,7 @@ "outDir": "./dist", // Source root directory - "rootDir": "./src" + "rootDir": "." }, // Files included in compilation diff --git a/packages/web/wasm_x86_i686.txt b/packages/web/wasm_x86_i686.txt index 215ef962..425f8127 100644 --- a/packages/web/wasm_x86_i686.txt +++ b/packages/web/wasm_x86_i686.txt @@ -12,7 +12,7 @@ exe_suffix = 'js' [built-in options] cpp_args = ['-Wshift-negative-value', '-flto', '-Os', '-fno-exceptions', '-ffunction-sections', '-fdata-sections'] -cpp_link_args = ['-Wshift-negative-value', '-flto', '-Os', '--bind', '-sWASM=1', '-sALLOW_MEMORY_GROWTH=1', '-sFORCE_FILESYSTEM=0', '-sMODULARIZE=1', '-sEXPORT_NAME=createRendererModule', '-sEXPORT_ES6=1', '-sUSE_ES6_IMPORT_META=1', '-sENVIRONMENT=web', '-sFILESYSTEM=0', '--no-entry', '--strip-all', '--minify=0'] +cpp_link_args = ['-Wshift-negative-value', '-flto', '-Os', '--bind', '-sWASM=1', '-sALLOW_MEMORY_GROWTH=1', '-sFORCE_FILESYSTEM=0', '-sMODULARIZE=1', '-sEXPORT_NAME=createRendererModule', '-sEXPORT_ES6=1', '-sUSE_ES6_IMPORT_META=0', '-sENVIRONMENT=web', '-sFILESYSTEM=0', '--no-entry', '--strip-all', '--minify=0', '-sDYNAMIC_EXECUTION=0'] [host_machine] system = 'emscripten'