From ad37a4fd9ae8f2ba27874fd6636c51e09250a554 Mon Sep 17 00:00:00 2001 From: Abdelrahman Ashraf Date: Tue, 17 Dec 2024 13:02:43 +0700 Subject: [PATCH] =?UTF-8?q?feat(WASM):=20=F0=9F=8E=B8=20implement=20playba?= =?UTF-8?q?ck=20observer=20callbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 +- dotlottie-ffi/emscripten_bindings.cpp | 67 +----- dotlottie-ffi/emscripten_library.js | 33 +++ dotlottie-ffi/src/dotlottie_player_cpp.udl | 2 + dotlottie-rs/src/dotlottie_player.rs | 243 +++++++++++++++++---- web-example.html | 54 +++-- 6 files changed, 273 insertions(+), 128 deletions(-) create mode 100644 dotlottie-ffi/emscripten_library.js diff --git a/Makefile b/Makefile index 2e740eed..f9addb5c 100644 --- a/Makefile +++ b/Makefile @@ -279,7 +279,7 @@ cpp_link_args = [ '--no-entry', '--strip-all', '--emit-tsd=${WASM_MODULE}.d.ts', - '--closure=1'] + '--js-library=$(PROJECT_DIR)/$(RUNTIME_FFI)/emscripten_library.js'] [host_machine] system = '$(SYSTEM)' diff --git a/dotlottie-ffi/emscripten_bindings.cpp b/dotlottie-ffi/emscripten_bindings.cpp index 1e8b8967..219097ee 100644 --- a/dotlottie-ffi/emscripten_bindings.cpp +++ b/dotlottie-ffi/emscripten_bindings.cpp @@ -5,18 +5,6 @@ using namespace emscripten; using namespace dotlottie_player; -extern "C" -{ - /* - This is a workaround as instant crate expects a _ prefix for the emscripten_get_now function - https://github.com/sebcrozet/instant/issues/35 - */ - double _emscripten_get_now() - { - return emscripten_get_now(); - } -} - val buffer(DotLottiePlayer &player) { auto buffer_ptr = player.buffer_ptr(); @@ -38,8 +26,6 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) register_vector("VectorFloat"); register_vector("VectorMarker"); register_vector("VectorString"); - // register_vector("VectorManifestTheme"); - // register_vector("VectorManifestAnimation"); enum_("Mode") .value("Forward", Mode::kForward) @@ -81,52 +67,6 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) function("createDefaultConfig", &create_default_config); function("transformThemeToLottieSlots", &transform_theme_to_lottie_slots); - // value_object("ManifestTheme") - // .field("id", &ManifestTheme::id) - // .field("animations", &ManifestTheme::animations); - - // value_object("ManifestAnimation") - // .field("autoplay", &ManifestAnimation::autoplay) - // .field("defaultTheme", &ManifestAnimation::default_theme) - // .field("direction", &ManifestAnimation::direction) - // .field("hover", &ManifestAnimation::hover) - // .field("id", &ManifestAnimation::id) - // .field("intermission", &ManifestAnimation::intermission) - // .field("loop", &ManifestAnimation::loop) - // .field("loop_count", &ManifestAnimation::loop_count) - // .field("playMode", &ManifestAnimation::play_mode) - // .field("speed", &ManifestAnimation::speed) - // .field("themeColor", &ManifestAnimation::theme_color); - - // value_object("Manifest") - // .field("active_animation_id", &Manifest::active_animation_id) - // .field("animations", &Manifest::animations) - // .field("author", &Manifest::author) - // .field("description", &Manifest::description) - // .field("generator", &Manifest::generator) - // .field("keywords", &Manifest::keywords) - // .field("revision", &Manifest::revision) - // .field("themes", &Manifest::themes) - // .field("states", &Manifest::states) - // .field("version", &Manifest::version); - - // class_("Observer") - // .smart_ptr>("Observer") - // .function("onFrame", &Observer::on_frame) - // .function("onLoad", &Observer::on_load) - // .function("onLoop", &Observer::on_loop) - // .function("onPause", &Observer::on_pause) - // .function("onPlay", &Observer::on_play) - // .function("onRender", &Observer::on_render) - // .function("onComplete", &Observer::on_complete) - // .function("onStop", &Observer::on_stop); - - // class_("StateMachineObserver") - // .smart_ptr>("StateMachineObserver") - // .function("OnTransition", &StateMachineObserver::on_transition); - // .function("onStateEntered", &StateMachineObserver::on_state_entered); - // .function("onStateExit", &StateMachineObserver::on_state_exit); - class_("DotLottiePlayer") .smart_ptr>("DotLottiePlayer") .constructor(&DotLottiePlayer::init, allow_raw_pointers()) @@ -156,8 +96,6 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) .function("seek", &DotLottiePlayer::seek) .function("stop", &DotLottiePlayer::stop) .function("totalFrames", &DotLottiePlayer::total_frames) - // .function("subscribe", &DotLottiePlayer::subscribe) - // .function("unsubscribe", &DotLottiePlayer::unsubscribe) .function("isComplete", &DotLottiePlayer::is_complete) .function("setTheme", &DotLottiePlayer::set_theme) .function("setThemeData", &DotLottiePlayer::set_theme_data) @@ -187,7 +125,6 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) .function("postPointerMoveEvent", &DotLottiePlayer::post_pointer_move_event) .function("postPointerEnterEvent", &DotLottiePlayer::post_pointer_enter_event) .function("postPointerExitEvent", &DotLottiePlayer::post_pointer_exit_event) - .function("postSetNumericContext", &DotLottiePlayer::post_set_numeric_context); - // .function("state_machine_subscribe", &DotLottiePlayer::state_machine_subscribe) - // .function("state_machine_unsubscribe", &DotLottiePlayer::state_machine_unsubscribe) + .function("postSetNumericContext", &DotLottiePlayer::post_set_numeric_context) + .function("instanceId", &DotLottiePlayer::instance_id); } diff --git a/dotlottie-ffi/emscripten_library.js b/dotlottie-ffi/emscripten_library.js new file mode 100644 index 00000000..552f5040 --- /dev/null +++ b/dotlottie-ffi/emscripten_library.js @@ -0,0 +1,33 @@ +// _DOTLOTTIE_BRIDGE_JS is a global object which will hold the implementation of the bridge functions +mergeInto(LibraryManager.library, { + _emscripten_get_now: function () { + return Date.now(); + }, + observer_on_load(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_load(dotlottie_instance_id); + }, + observer_on_load_error(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_load_error(dotlottie_instance_id); + }, + observer_on_play(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_play(dotlottie_instance_id); + }, + observer_on_pause(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_pause(dotlottie_instance_id); + }, + observer_on_stop(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_stop(dotlottie_instance_id); + }, + observer_on_frame(dotlottie_instance_id, frame_no) { + _DOTLOTTIE_BRIDGE_JS.observer_on_frame(dotlottie_instance_id, frame_no); + }, + observer_on_render(dotlottie_instance_id, frame_no) { + _DOTLOTTIE_BRIDGE_JS.observer_on_render(dotlottie_instance_id, frame_no); + }, + observer_on_loop(dotlottie_instance_id, loop_count) { + _DOTLOTTIE_BRIDGE_JS.observer_on_loop(dotlottie_instance_id, loop_count); + }, + observer_on_complete(dotlottie_instance_id) { + _DOTLOTTIE_BRIDGE_JS.observer_on_complete(dotlottie_instance_id); + }, +}); diff --git a/dotlottie-ffi/src/dotlottie_player_cpp.udl b/dotlottie-ffi/src/dotlottie_player_cpp.udl index cabc1ab7..288eaff3 100644 --- a/dotlottie-ffi/src/dotlottie_player_cpp.udl +++ b/dotlottie-ffi/src/dotlottie_player_cpp.udl @@ -102,4 +102,6 @@ interface DotLottiePlayer { i32 post_pointer_enter_event(f32 x, f32 y); i32 post_pointer_exit_event(f32 x, f32 y); i32 post_set_numeric_context([ByRef] string key, f32 value); + + u32 instance_id(); }; diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 83077b38..83a31cb5 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -26,6 +26,21 @@ pub trait Observer: Send + Sync { fn on_complete(&self); } +#[cfg(target_arch = "wasm32")] +mod wasm_observer_callbacks_ffi { + extern "C" { + pub fn observer_on_load(dotlottie_instance_id: u32); + pub fn observer_on_load_error(dotlottie_instance_id: u32); + pub fn observer_on_play(dotlottie_instance_id: u32); + pub fn observer_on_pause(dotlottie_instance_id: u32); + pub fn observer_on_stop(dotlottie_instance_id: u32); + pub fn observer_on_frame(dotlottie_instance_id: u32, frame_no: f32); + pub fn observer_on_render(dotlottie_instance_id: u32, frame_no: f32); + pub fn observer_on_loop(dotlottie_instance_id: u32, loop_count: u32); + pub fn observer_on_complete(dotlottie_instance_id: u32); + } +} + pub enum PlaybackState { Playing, Paused, @@ -885,15 +900,23 @@ pub struct DotLottiePlayerContainer { runtime: RwLock, observers: RwLock>>, state_machine: Rc>>, + #[cfg(target_arch = "wasm32")] + instance_id: u32, } impl DotLottiePlayerContainer { #[cfg(feature = "thorvg")] pub fn new(config: Config) -> Self { + #[cfg(target_arch = "wasm32")] + static NEXT_INSTANCE_ID: std::sync::atomic::AtomicU32 = + std::sync::atomic::AtomicU32::new(1); + DotLottiePlayerContainer { runtime: RwLock::new(DotLottieRuntime::new(config)), observers: RwLock::new(Vec::new()), state_machine: Rc::new(RwLock::new(None)), + #[cfg(target_arch = "wasm32")] + instance_id: NEXT_INSTANCE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), } } @@ -902,6 +925,152 @@ impl DotLottiePlayerContainer { runtime: RwLock::new(DotLottieRuntime::with_renderer(config, renderer)), observers: RwLock::new(Vec::new()), state_machine: Rc::new(RwLock::new(None)), + #[cfg(target_arch = "wasm32")] + instance_id: 0, + } + } + + pub fn emit_on_load(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_load(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_load(self.instance_id); + } + } + } + + pub fn emit_on_load_error(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_load_error(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_load_error(self.instance_id); + } + } + } + + pub fn emit_on_play(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_play(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_play(self.instance_id); + } + } + } + + pub fn emit_on_pause(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_pause(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_pause(self.instance_id); + } + } + } + + pub fn emit_on_stop(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_stop(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_stop(self.instance_id); + } + } + } + + pub fn emit_on_frame(&self, frame_no: f32) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_frame(frame_no); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_frame(self.instance_id, frame_no); + } + } + } + + pub fn emit_on_render(&self, frame_no: f32) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_render(frame_no); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_render(self.instance_id, frame_no); + } + } + } + + pub fn emit_on_loop(&self, loop_count: u32) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_loop(loop_count); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_loop(self.instance_id, loop_count); + } + } + } + + pub fn emit_on_complete(&self) { + #[cfg(not(target_arch = "wasm32"))] + { + self.observers.read().unwrap().iter().for_each(|observer| { + observer.on_complete(); + }); + } + + #[cfg(target_arch = "wasm32")] + { + unsafe { + wasm_observer_callbacks_ffi::observer_on_complete(self.instance_id); + } } } @@ -912,17 +1081,13 @@ impl DotLottiePlayerContainer { .is_ok_and(|mut runtime| runtime.load_animation_data(animation_data, width, height)); if is_ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load(); - }); + self.emit_on_load(); if self.config().autoplay { self.play(); } } else { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load_error(); - }); + self.emit_on_load_error(); return false; } @@ -937,17 +1102,13 @@ impl DotLottiePlayerContainer { .is_ok_and(|mut runtime| runtime.load_animation_path(animation_path, width, height)); if is_ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load(); - }); + self.emit_on_load(); if self.config().autoplay { self.play(); } } else { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load_error(); - }); + self.emit_on_load_error(); return false; } @@ -962,17 +1123,13 @@ impl DotLottiePlayerContainer { .is_ok_and(|mut runtime| runtime.load_dotlottie_data(file_data, width, height)); if is_ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load(); - }); + self.emit_on_load(); if self.config().autoplay { self.play(); } } else { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load_error(); - }); + self.emit_on_load_error(); return false; } @@ -987,17 +1144,13 @@ impl DotLottiePlayerContainer { .is_ok_and(|mut runtime| runtime.load_animation(animation_id, width, height)); if is_ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load(); - }); + self.emit_on_load(); if self.config().autoplay { self.play(); } } else { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_load_error(); - }); + self.emit_on_load_error(); return false; } @@ -1080,9 +1233,7 @@ impl DotLottiePlayerContainer { let ok = self.runtime.write().unwrap().play(); if ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_play(); - }); + self.emit_on_play(); } ok @@ -1092,9 +1243,7 @@ impl DotLottiePlayerContainer { let ok = self.runtime.write().unwrap().pause(); if ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_pause(); - }); + self.emit_on_pause(); } ok @@ -1104,9 +1253,7 @@ impl DotLottiePlayerContainer { let ok = self.runtime.write().unwrap().stop(); if ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_stop(); - }); + self.emit_on_stop(); } ok @@ -1120,9 +1267,7 @@ impl DotLottiePlayerContainer { let ok = self.runtime.write().unwrap().set_frame(no); if ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_frame(no); - }); + self.emit_on_frame(no); } ok @@ -1132,9 +1277,7 @@ impl DotLottiePlayerContainer { let ok = self.runtime.write().unwrap().seek(no); if ok { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_frame(no); - }); + self.emit_on_frame(no); } ok @@ -1146,19 +1289,13 @@ impl DotLottiePlayerContainer { if ok { let frame_no = self.current_frame(); - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_render(frame_no); - }); + self.emit_on_render(frame_no); if self.is_complete() { if self.config().loop_animation { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_loop(self.loop_count()); - }); + self.emit_on_loop(self.loop_count()); } else { - self.observers.read().unwrap().iter().for_each(|observer| { - observer.on_complete(); - }); + self.emit_on_complete(); if let Ok(mut state_machine) = self.state_machine.try_write() { if let Some(sm) = state_machine.as_mut() { @@ -1268,6 +1405,11 @@ impl DotLottiePlayerContainer { Err(_) => None, } } + + #[cfg(target_arch = "wasm32")] + pub fn instance_id(&self) -> u32 { + self.instance_id + } } pub struct DotLottiePlayer { @@ -1817,6 +1959,11 @@ impl DotLottiePlayer { pub fn animation_size(&self) -> Vec { self.player.read().unwrap().animation_size() } + + #[cfg(target_arch = "wasm32")] + pub fn instance_id(&self) -> u32 { + self.player.read().unwrap().instance_id() + } } unsafe impl Send for DotLottiePlayer {} diff --git a/web-example.html b/web-example.html index fe9b6bb7..dd669a98 100644 --- a/web-example.html +++ b/web-example.html @@ -129,6 +129,40 @@

Test

marker: "feather", }); + const instances = {}; + + instances[dotLottiePlayer.instanceId()] = dotLottiePlayer; + + globalThis._DOTLOTTIE_BRIDGE_JS = { + observer_on_load: (dotlottie_instance_id) => { + console.log("Loaded", instances[dotlottie_instance_id]); + }, + observer_on_load_error: (dotlottie_instance_id) => { + console.log("Error", instances[dotlottie_instance_id]); + }, + observer_on_play: (dotlottie_instance_id) => { + console.log("Play", instances[dotlottie_instance_id]); + }, + observer_on_pause: (dotlottie_instance_id) => { + console.log("Pause", instances[dotlottie_instance_id]); + }, + observer_on_stop: (dotlottie_instance_id) => { + console.log("Stop", instances[dotlottie_instance_id]); + }, + observer_on_frame: (dotlottie_instance_id, frame_no) => { + console.log("Frame", instances[dotlottie_instance_id], frame_no); + }, + observer_on_render: (dotlottie_instance_id, frame_no) => { + console.log("Render", instances[dotlottie_instance_id], frame_no); + }, + observer_on_loop: (dotlottie_instance_id, loop_count) => { + console.log("Loop", instances[dotlottie_instance_id], loop_count); + }, + observer_on_complete: (dotlottie_instance_id) => { + console.log("Complete", instances[instance_id]); + }, + }; + const data = await fetch( // "https://lottie.host/5c89381e-0d1a-4422-8247-f5b7e4b3c4e2/mqs5juC4PW.lottie" "./examples/demo-player/src/v2/gradient.json" @@ -169,8 +203,6 @@

Test

console.log(dotLottiePlayer.markers()); - console.log("Loaded: ", loaded); - const ctx = canvas.getContext("2d"); let animationFrameId = null; @@ -178,22 +210,16 @@

Test

function animationLoop() { const nextFrameNumber = dotLottiePlayer.requestFrame(); - const updated = dotLottiePlayer.setFrame(nextFrameNumber); + const updated = dotLottiePlayer.setFrame(parseInt(nextFrameNumber)); if (updated) { - progressBar.value = - (dotLottiePlayer.currentFrame() / dotLottiePlayer.totalFrames()) * - 100; + setTimeout(() => { + progressBar.value = + (dotLottiePlayer.currentFrame() / dotLottiePlayer.totalFrames()) * + 100; + }, 0); render(); - - if (dotLottiePlayer.isComplete()) { - if (dotLottiePlayer.config().loopAnimation) { - console.log("Loop Completed", dotLottiePlayer.loopCount()); - } else { - console.log("Completed"); - } - } } animationFrameId = requestAnimationFrame(animationLoop);