diff --git a/dotlottie-ffi/emscripten_bindings.cpp b/dotlottie-ffi/emscripten_bindings.cpp index a4703d01..d9489678 100644 --- a/dotlottie-ffi/emscripten_bindings.cpp +++ b/dotlottie-ffi/emscripten_bindings.cpp @@ -179,6 +179,9 @@ EMSCRIPTEN_BINDINGS(DotLottiePlayer) .function("stateMachineSetNumericTrigger", &DotLottiePlayer::state_machine_set_numeric_trigger) .function("stateMachineSetStringTrigger", &DotLottiePlayer::state_machine_set_string_trigger) .function("stateMachineSetBooleanTrigger", &DotLottiePlayer::state_machine_set_boolean_trigger) + .function("stateMachineGetNumericTrigger", &DotLottiePlayer::state_machine_get_numeric_trigger) + .function("stateMachineGetStringTrigger", &DotLottiePlayer::state_machine_get_string_trigger) + .function("stateMachineGetBooleanTrigger", &DotLottiePlayer::state_machine_get_boolean_trigger) .function("getLayerBounds", &DotLottiePlayer::get_layer_bounds) .function("stateMachineCurrentState", &DotLottiePlayer::state_machine_current_state) .function("stateMachinePostPointerDownEvent", &DotLottiePlayer::state_machine_post_pointer_down_event) diff --git a/dotlottie-ffi/src/dotlottie_player.udl b/dotlottie-ffi/src/dotlottie_player.udl index 36f14ed7..ea291b11 100644 --- a/dotlottie-ffi/src/dotlottie_player.udl +++ b/dotlottie-ffi/src/dotlottie_player.udl @@ -163,6 +163,9 @@ interface DotLottiePlayer { boolean state_machine_set_boolean_trigger([ByRef] string key, boolean value); boolean state_machine_set_string_trigger([ByRef] string key, [ByRef] string value); boolean state_machine_set_numeric_trigger([ByRef] string key, f32 value); + f32 state_machine_get_numeric_trigger([ByRef] string key); + string state_machine_get_string_trigger([ByRef] string key); + boolean state_machine_get_boolean_trigger([ByRef] string key); string state_machine_current_state(); boolean state_machine_subscribe(StateMachineObserver observer); boolean state_machine_unsubscribe([ByRef] StateMachineObserver observer); diff --git a/dotlottie-ffi/src/dotlottie_player_cpp.udl b/dotlottie-ffi/src/dotlottie_player_cpp.udl index 9cdad686..6674f560 100644 --- a/dotlottie-ffi/src/dotlottie_player_cpp.udl +++ b/dotlottie-ffi/src/dotlottie_player_cpp.udl @@ -100,4 +100,7 @@ interface DotLottiePlayer { boolean state_machine_set_boolean_trigger([ByRef] string key, boolean value); boolean state_machine_set_string_trigger([ByRef] string key, [ByRef] string value); boolean state_machine_set_numeric_trigger([ByRef] string key, f32 value); + f32 state_machine_get_numeric_trigger([ByRef] string key); + string state_machine_get_string_trigger([ByRef] string key); + boolean state_machine_get_boolean_trigger([ByRef] string key); }; diff --git a/dotlottie-ffi/src/ffi/mod.rs b/dotlottie-ffi/src/ffi/mod.rs index 9536c324..41e4c675 100644 --- a/dotlottie-ffi/src/ffi/mod.rs +++ b/dotlottie-ffi/src/ffi/mod.rs @@ -690,6 +690,51 @@ pub unsafe extern "C" fn dotlottie_state_machine_set_boolean_trigger( }) } +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_boolean_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// ) -> bool { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player.state_machine_get_boolean_trigger(&key) +// } else { +// false +// } +// }) +// } + +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_string_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// result: *mut types::DotLottieString, +// ) -> i32 { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player +// .state_machine_get_string_trigger(&key) +// .copy(result); +// } else { +// DOTLOTTIE_INVALID_PARAMETER +// } +// }) +// } + +// #[no_mangle] +// pub unsafe extern "C" fn dotlottie_state_machine_get_numeric_trigger( +// ptr: *mut DotLottiePlayer, +// key: *const c_char, +// ) -> f32 { +// exec_dotlottie_player_op(ptr, |dotlottie_player| { +// if let Ok(key) = DotLottieString::read(key) { +// dotlottie_player.state_machine_get_numeric_trigger(&key) +// } else { +// f32::MIN +// } +// }) +// } + #[no_mangle] pub unsafe extern "C" fn dotlottie_state_machine_framework_setup( ptr: *mut DotLottiePlayer, diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 7f6af20b..e7a49fa9 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -1392,21 +1392,28 @@ impl DotLottiePlayer { listener_types.push("PointerDown".to_string()) } crate::listeners::Listener::PointerEnter { .. } => { - // Push PointerMove so that can determine if the pointer entered the layer - listener_types.push("PointerMove".to_string()) + // In case framework self detects pointer entering layers, push pointerExit + listener_types.push("PointerEnter".to_string()); + // We push PointerMove too so that we can do hit detection instead of the framework + listener_types.push("PointerMove".to_string()); } crate::listeners::Listener::PointerMove { .. } => { listener_types.push("PointerMove".to_string()) } crate::listeners::Listener::PointerExit { .. } => { - // Push PointerMove so that can determine if the pointer exited the layer - listener_types.push("PointerMove".to_string()) + // In case framework self detects pointer exiting layers, push pointerExit + listener_types.push("PointerExit".to_string()); + // We push PointerMove too so that we can do hit detection instead of the framework + listener_types.push("PointerMove".to_string()); } crate::listeners::Listener::OnComplete { .. } => { listener_types.push("OnComplete".to_string()) } } } + + listener_types.sort(); + listener_types.dedup(); listener_types } else { vec![] @@ -1518,6 +1525,53 @@ impl DotLottiePlayer { } } + pub fn state_machine_get_numeric_trigger(&self, key: &str) -> f32 { + match self.state_machine.try_read() { + Ok(state_machine) => { + if let Some(sm) = &*state_machine { + if let Some(value) = sm.get_numeric_trigger(key) { + return value; + } + } + } + Err(_) => { + return f32::MIN; + } + } + + f32::MIN + } + + pub fn state_machine_get_string_trigger(&self, key: &str) -> String { + match self.state_machine.try_write() { + Ok(mut state_machine) => { + if let Some(sm) = state_machine.as_mut() { + if let Some(value) = sm.get_string_trigger(key) { + return value; + } + } + } + Err(_) => return "".to_string(), + } + + "".to_string() + } + + pub fn state_machine_get_boolean_trigger(&self, key: &str) -> bool { + match self.state_machine.try_write() { + Ok(mut state_machine) => { + if let Some(sm) = state_machine.as_mut() { + if let Some(value) = sm.get_boolean_trigger(key) { + return value; + } + } + } + Err(_) => return false, + } + + false + } + pub fn state_machine_fire_event(&self, event: &str) { if let Ok(mut state_machine) = self.state_machine.try_write() { if let Some(sm) = state_machine.as_mut() { @@ -1810,6 +1864,7 @@ impl DotLottiePlayer { return "".to_string(); } } + "".to_string() } } diff --git a/dotlottie-rs/src/state_machine_engine/actions/mod.rs b/dotlottie-rs/src/state_machine_engine/actions/mod.rs index 67ad42d8..3f152b3e 100644 --- a/dotlottie-rs/src/state_machine_engine/actions/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/actions/mod.rs @@ -23,6 +23,9 @@ pub trait ActionTrait { ) -> Result<(), StateMachineActionError>; } +// Todo: +// - FireCustomEvent +// - Reset #[derive(Debug, Deserialize, Clone)] #[serde(rename_all_fields = "camelCase")] #[serde(tag = "type")] @@ -30,9 +33,6 @@ pub enum Action { OpenUrl { url: String, }, - Theme { - theme_id: String, - }, Increment { trigger_name: String, value: Option, @@ -215,14 +215,14 @@ impl ActionTrait for Action { Ok(()) } - // Todo: Add support for setting a trigger to a trigger value Action::Fire { trigger_name } => { let _ = engine.fire(trigger_name, run_pipeline); Ok(()) } - Action::Reset { trigger_name } => { - todo!("Reset trigger {}", trigger_name); - // Ok(()) + Action::Reset { trigger_name: _ } => { + // todo!("Reset trigger {}", trigger_name); + + Ok(()) } Action::SetExpression { layer_name, @@ -360,23 +360,6 @@ impl ActionTrait for Action { Ok(()) } - Action::Theme { theme_id } => { - let read_lock = player.read(); - - match read_lock { - Ok(player) => { - if !player.set_theme(theme_id) { - return Err(StateMachineActionError::ExecuteError( - "Error loading theme".to_string(), - )); - } - Ok(()) - } - Err(_) => Err(StateMachineActionError::ExecuteError( - "Error getting read lock on player".to_string(), - )), - } - } } } } diff --git a/dotlottie-rs/src/state_machine_engine/events/mod.rs b/dotlottie-rs/src/state_machine_engine/events/mod.rs index 4387a16b..176fec6e 100644 --- a/dotlottie-rs/src/state_machine_engine/events/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/events/mod.rs @@ -51,3 +51,25 @@ impl EventName for Event { } } } + +#[macro_export] +macro_rules! event_type_name { + (PointerDown) => { + "PointerDown" + }; + (PointerUp) => { + "PointerUp" + }; + (PointerMove) => { + "PointerMove" + }; + (PointerEnter) => { + "PointerEnter" + }; + (PointerExit) => { + "PointerExit" + }; + (OnComplete) => { + "OnComplete" + }; +} diff --git a/dotlottie-rs/src/state_machine_engine/listeners/mod.rs b/dotlottie-rs/src/state_machine_engine/listeners/mod.rs index a9d3eed4..4a2093e2 100644 --- a/dotlottie-rs/src/state_machine_engine/listeners/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/listeners/mod.rs @@ -35,7 +35,6 @@ pub enum Listener { actions: Vec, }, PointerMove { - layer_name: Option, actions: Vec, }, PointerExit { @@ -75,12 +74,8 @@ impl Display for Listener { .field("layer_name", layer_name) .field("action", actions) .finish(), - Self::PointerMove { - layer_name, - actions, - } => f + Self::PointerMove { actions } => f .debug_struct("PointerUp") - .field("layer_name", layer_name) .field("action", actions) .finish(), Self::PointerExit { @@ -109,7 +104,7 @@ impl ListenerTrait for Listener { Listener::PointerUp { layer_name, .. } => layer_name.clone(), Listener::PointerDown { layer_name, .. } => layer_name.clone(), Listener::PointerEnter { layer_name, .. } => layer_name.clone(), - Listener::PointerMove { layer_name, .. } => layer_name.clone(), + Listener::PointerMove { .. } => None, Listener::PointerExit { layer_name, .. } => layer_name.clone(), Listener::OnComplete { .. } => None, } diff --git a/dotlottie-rs/src/state_machine_engine/mod.rs b/dotlottie-rs/src/state_machine_engine/mod.rs index 3956da7e..3ddab41e 100644 --- a/dotlottie-rs/src/state_machine_engine/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/mod.rs @@ -24,8 +24,8 @@ use triggers::Trigger; use crate::state_machine_engine::listeners::Listener; use crate::{ - state_machine_state_check_pipeline, DotLottiePlayerContainer, EventName, PointerEvent, - StateMachineEngineSecurityError, + event_type_name, state_machine_state_check_pipeline, DotLottiePlayerContainer, EventName, + PointerEvent, StateMachineEngineSecurityError, }; use self::state_machine::state_machine_parse; @@ -78,8 +78,6 @@ pub enum StateMachineEngineError { } pub struct StateMachineEngine { - // pub listeners: Vec, - /* We keep references to the StateMachine's States. */ /* This prevents duplicating the data inside the engine. */ pub global_state: Option>, @@ -94,6 +92,10 @@ pub struct StateMachineEngine { event_trigger: HashMap, curr_event: Option, + // PointerEnter/PointerExit management + curr_entered_layer: String, + listened_layers: Vec<(String, String)>, + observers: RwLock>>, state_machine: StateMachine, @@ -116,6 +118,8 @@ impl Default for StateMachineEngine { boolean_trigger: HashMap::new(), event_trigger: HashMap::new(), curr_event: None, + curr_entered_layer: "".to_string(), + listened_layers: Vec::new(), status: StateMachineEngineStatus::Stopped, observers: RwLock::new(Vec::new()), state_history: Vec::new(), @@ -158,6 +162,8 @@ impl StateMachineEngine { boolean_trigger: HashMap::new(), event_trigger: HashMap::new(), curr_event: None, + curr_entered_layer: "".to_string(), + listened_layers: Vec::new(), status: StateMachineEngineStatus::Stopped, observers: RwLock::new(Vec::new()), state_history: Vec::new(), @@ -281,10 +287,10 @@ impl StateMachineEngine { let mut new_state_machine = StateMachineEngine::default(); if parsed_state_machine.is_err() { - // println!( - // "Error parsing state machine definition: {:?}", - // parsed_state_machine.err() - // ); + println!( + "Error parsing state machine definition: {:?}", + parsed_state_machine.err() + ); return Err(StateMachineEngineError::ParsingError { reason: "Failed to parse state machine definition".to_string(), }); @@ -329,6 +335,8 @@ impl StateMachineEngine { new_state_machine.player = Some(player.clone()); new_state_machine.state_machine = parsed_state_machine; + new_state_machine.init_listened_layers(); + // Run the security check pipeline let check_report = self.security_check_pipeline(&new_state_machine); @@ -398,9 +406,9 @@ impl StateMachineEngine { self.current_state.clone() } - pub fn listeners(&self, filter: Option) -> Vec<&Listener> { + pub fn listeners(&self, event_type_filter: Option) -> Vec<&Listener> { let mut listeners_clone = Vec::new(); - let filter = filter.unwrap_or("".to_string()); + let filter = event_type_filter.unwrap_or("".to_string()); if let Some(listeners) = &self.state_machine.listeners { for listener in listeners { @@ -420,6 +428,49 @@ impl StateMachineEngine { listeners_clone } + fn init_listened_layers(&mut self) { + let mut listeners = vec![]; + + listeners.extend(self.listeners(None)); + + let mut all_listened_layers: Vec<(String, String)> = vec![]; + + // Get every layer we listen to + for listener in listeners { + match listener { + Listener::PointerEnter { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerEnter).to_string())); + } + } + Listener::PointerExit { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerExit).to_string())) + } + } + Listener::PointerUp { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerUp).to_string())) + } + } + Listener::PointerDown { layer_name, .. } => { + if let Some(layer) = layer_name { + all_listened_layers + .push((layer.clone(), event_type_name!(PointerDown).to_string())) + } + } + _ => {} + } + } + + self.listened_layers = all_listened_layers; + + println!("Configured listened_layers : {:?}", self.listened_layers); + } + fn get_state(&self, state_name: &str) -> Option> { if let Some(global_state) = &self.global_state { if global_state.name() == state_name { @@ -622,7 +673,7 @@ impl StateMachineEngine { self.current_cycle_count += 1; if self.current_cycle_count >= self.max_cycle_count { - // println!("🚨 Infinite loop detected, ending state machine."); + println!("🚨 Infinite loop detected, ending state machine."); self.end(); return Err(StateMachineEngineError::InfiniteLoopError); } @@ -732,18 +783,17 @@ impl StateMachineEngine { None } - fn get_correct_pointer_actions_from_listener( + fn is_listener_active( &self, event: &Event, - layer_name: Option, - actions: &Vec, + listener: &Listener, x: f32, y: f32, ) -> Vec { let mut actions_to_execute = Vec::new(); // User defined a specific layer to check if hit - if let Some(layer) = layer_name { + if let Some(layer) = listener.get_layer_name() { // Check if the layer was hit, otherwise we ignore this listener if let Some(rc_player) = &self.player { let try_read_lock = rc_player.try_read(); @@ -752,31 +802,25 @@ impl StateMachineEngine { // If we have a pointer down event, we need to check if the pointer is outside of the layer if let Event::PointerExit { x, y } = event { if !player_container.hit_check(&layer, *x, *y) { - for action in actions { - actions_to_execute.push(action.clone()); - } + actions_to_execute.extend(listener.get_actions().clone()); } } else { // Hit check will return true if the layer was hit if player_container.hit_check(&layer, x, y) { - for action in actions { - actions_to_execute.push(action.clone()); - } + actions_to_execute.extend(listener.get_actions().clone()); } } } } } else { // No layer was specified, add all actions - for action in actions { - actions_to_execute.push(action.clone()); - } + actions_to_execute.extend(listener.get_actions().clone()); } actions_to_execute } - fn manage_pointer_event(&mut self, event: &Event, x: f32, y: f32) { + fn manage_pointer_down_up_enter_exit(&mut self, event: &Event, x: f32, y: f32) { let listeners = self.listeners(Some(event.type_name())); if listeners.is_empty() { @@ -786,13 +830,7 @@ impl StateMachineEngine { let mut actions_to_execute = Vec::new(); for listener in listeners { - let action_vec = self.get_correct_pointer_actions_from_listener( - event, - listener.get_layer_name(), - listener.get_actions(), - x, - y, - ); + let action_vec = self.is_listener_active(event, &listener, x, y); // Action vec was moved in to action_to_execute, it can't be used again actions_to_execute.extend(action_vec); @@ -806,6 +844,85 @@ impl StateMachineEngine { } } + fn manage_pointer_event(&mut self, event: &Event, x: f32, y: f32) { + // This will handle PointerDown, PointerUp, PointerEnter, PointerExit + if event.type_name() != "PointerMove" { + self.manage_pointer_down_up_enter_exit(event, x, y); + } + + // We're left with PointerMove + if event.type_name() == "PointerMove" { + let mut actions_to_execute = Vec::new(); + + if let Some(rc_player) = &self.player { + let try_read_lock = rc_player.try_read(); + + if let Ok(player_container) = try_read_lock { + // Check if we've moved the mouse over any of the pointerEnter/Exit listeners + // If we've changed layers, perform exit actions + // If we don't hit any layers, perform exit actions + + let mut hit = false; + let old_layer = self.curr_entered_layer.clone(); + + for (layer, _) in &self.listened_layers { + if player_container.hit_check(&layer, x, y) { + hit = true; + + if self.curr_entered_layer == *layer { + break; + } + + self.curr_entered_layer = layer.to_string(); + + let pointer_enter_listeners = + self.listeners(Some(event_type_name!(PointerEnter).to_string())); + + for listener in pointer_enter_listeners { + if let Some(listener_layer_name) = listener.get_layer_name() { + if *listener_layer_name == self.curr_entered_layer { + actions_to_execute.extend(listener.get_actions().clone()); + } + } + } + } + } + + if !hit { + self.curr_entered_layer = "".to_string(); + + let pointer_enter_listeners = + self.listeners(Some(event_type_name!(PointerExit).to_string())); + + for listener in pointer_enter_listeners { + if let Some(listener_layer_name) = listener.get_layer_name() { + if *listener_layer_name == old_layer { + actions_to_execute.extend(listener.get_actions().clone()); + } + } + } + } + + let pointer_move_listener = + self.listeners(Some(event_type_name!(PointerMove).to_string())); + + // Manage actual pointerMove listeners + for listener in pointer_move_listener { + if let Listener::PointerMove { actions } = listener { + actions_to_execute.extend(actions.clone()); + } + } + } + } + for action in actions_to_execute { + // Run the pipeline because listeners are outside of the evaluation pipeline loop + if let Some(player_ref) = &self.player { + let _ = action.execute(self, player_ref.clone(), true); + } + } + } + } + fn manage_on_complete_event(&mut self, event: &Event) { let listeners = self.listeners(Some(event.type_name())); diff --git a/dotlottie-rs/src/state_machine_engine/states/mod.rs b/dotlottie-rs/src/state_machine_engine/states/mod.rs index a049caec..52b5043c 100644 --- a/dotlottie-rs/src/state_machine_engine/states/mod.rs +++ b/dotlottie-rs/src/state_machine_engine/states/mod.rs @@ -121,7 +121,8 @@ impl StateTrait for State { let size = player_read.size(); // Todo compare against currently loaded animation - if !animation_id.is_empty() { + if !animation_id.is_empty() && player_read.active_animation_id() != *animation_id + { player_read.load_animation(animation_id, size.0, size.1); } diff --git a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json similarity index 95% rename from dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json rename to dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json index ad5bdb6c..ddba6181 100644 --- a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter_exit.json +++ b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_enter.json @@ -190,17 +190,6 @@ "value": 5 } ] - }, - { - "type": "PointerExit", - "layerName": "star5", - "actions": [ - { - "type": "SetNumeric", - "triggerName": "rating", - "value": 0 - } - ] } ], "triggers": [ diff --git a/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json new file mode 100644 index 00000000..90207857 --- /dev/null +++ b/dotlottie-rs/tests/fixtures/statemachines/listener_tests/pointer_exit.json @@ -0,0 +1,202 @@ +{ + "descriptor": { + "id": "star-rating", + "initial": "global" + }, + "states": [ + { + "name": "global", + "type": "GlobalState", + "animationId": "", + "transitions": [ + { + "type": "Transition", + "toState": "star_0", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 0 + } + ] + }, + { + "type": "Transition", + "toState": "star_1", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 1 + } + ] + }, + { + "type": "Transition", + "toState": "star_2", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 2 + } + ] + }, + { + "type": "Transition", + "toState": "star_3", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 3 + } + ] + }, + { + "type": "Transition", + "toState": "star_4", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 4 + } + ] + }, + { + "type": "Transition", + "toState": "star_5", + "guards": [ + { + "type": "Numeric", + "conditionType": "Equal", + "triggerName": "rating", + "compareTo": 5 + } + ] + } + ] + }, + { + "type": "PlaybackState", + "name": "star_0", + "animationId": "", + "autoplay": true, + "segment": "star_0", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_1", + "animationId": "", + "autoplay": true, + "segment": "star_1", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_2", + "animationId": "", + "autoplay": true, + "segment": "star_2", + "transitions": [], + "entryActions": [] + }, + { + "type": "PlaybackState", + "name": "star_3", + "animationId": "", + "autoplay": true, + "segment": "star_3", + "transitions": [] + }, + { + "type": "PlaybackState", + "name": "star_4", + "animationId": "", + "autoplay": true, + "segment": "star_4", + "transitions": [] + }, + { + "type": "PlaybackState", + "name": "star_5", + "animationId": "", + "autoplay": true, + "segment": "star_5", + "transitions": [] + } + ], + "listeners": [ + { + "type": "PointerExit", + "layerName": "star1", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 1 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star2", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 2 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star3", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 3 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star4", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 4 + } + ] + }, + { + "type": "PointerExit", + "layerName": "star5", + "actions": [ + { + "type": "SetNumeric", + "triggerName": "rating", + "value": 5 + } + ] + } + ], + "triggers": [ + { + "type": "Numeric", + "name": "rating", + "value": 0 + } + ] +} \ No newline at end of file diff --git a/dotlottie-rs/tests/state_machine_actions.rs b/dotlottie-rs/tests/state_machine_actions.rs index de363c61..1765131e 100644 --- a/dotlottie-rs/tests/state_machine_actions.rs +++ b/dotlottie-rs/tests/state_machine_actions.rs @@ -271,10 +271,6 @@ mod tests { } // TODO - #[test] - fn theme_action() { // todo!() - } - #[test] fn fire_custom_event() { // todo!() diff --git a/dotlottie-rs/tests/state_machine_listeners.rs b/dotlottie-rs/tests/state_machine_listeners.rs index 18baf230..eff14726 100644 --- a/dotlottie-rs/tests/state_machine_listeners.rs +++ b/dotlottie-rs/tests/state_machine_listeners.rs @@ -87,9 +87,8 @@ mod tests { } #[test] - pub fn pointer_enter_exit_test() { - let global_state = - include_str!("fixtures/statemachines/listener_tests/pointer_enter_exit.json"); + pub fn pointer_enter_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_enter.json"); let player = DotLottiePlayer::new(Config::default()); player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); let l = player.state_machine_load_data(global_state); @@ -120,16 +119,113 @@ mod tests { player.state_machine_post_event(&Event::PointerEnter { x: 75.0, y: 45.0 }); let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_5"); + } + + #[test] + pub fn pointer_enter_via_move_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_enter.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); + + assert!(l); + assert!(s); - // This should keep rating at 5 since we're still in the last star - player.state_machine_post_event(&Event::PointerExit { x: 75.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); + + player.state_machine_post_event(&Event::PointerMove { x: 30.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_2"); + + player.state_machine_post_event(&Event::PointerMove { x: 45.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_3"); + + player.state_machine_post_event(&Event::PointerMove { x: 60.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_4"); + + player.state_machine_post_event(&Event::PointerMove { x: 75.0, y: 45.0 }); let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_5"); + } + + #[test] + pub fn pointer_exit_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_exit.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); - // This should no keep rating at 5 since we're not in the last star + assert!(l); + assert!(s); + + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerEnter { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); player.state_machine_post_event(&Event::PointerExit { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_5"); + } + + #[test] + pub fn pointer_exit_via_move_test() { + let global_state = include_str!("fixtures/statemachines/listener_tests/pointer_exit.json"); + let player = DotLottiePlayer::new(Config::default()); + player.load_dotlottie_data(include_bytes!("fixtures/star_marked.lottie"), 100, 100); + let l = player.state_machine_load_data(global_state); + let s = player.state_machine_start(); + + assert!(l); + assert!(s); + let curr_state_name = get_current_state_name(&player); assert_eq!(curr_state_name, "star_0"); + + player.state_machine_post_event(&Event::PointerMove { x: 15.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_0"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); + + player.state_machine_post_event(&Event::PointerMove { x: 30.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_1"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_2"); + + player.state_machine_post_event(&Event::PointerMove { x: 45.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_2"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_3"); + + player.state_machine_post_event(&Event::PointerMove { x: 60.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_3"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_4"); + + player.state_machine_post_event(&Event::PointerMove { x: 75.0, y: 45.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_4"); + player.state_machine_post_event(&Event::PointerMove { x: 0.0, y: 0.0 }); + let curr_state_name = get_current_state_name(&player); + assert_eq!(curr_state_name, "star_5"); } #[test] diff --git a/examples/demo-player/src/taa.json b/examples/demo-player/src/taa.json new file mode 100644 index 00000000..fe25cf38 --- /dev/null +++ b/examples/demo-player/src/taa.json @@ -0,0 +1 @@ +{"ddd":0,"h":1080,"w":1920,"meta":{"g":"@lottiefiles/toolkit-js 0.55.0"},"layers":[{"ty":4,"sr":1,"st":6,"op":312,"ip":6,"ln":"20","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[742.5,92.5,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[1702.5,632.5,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"i":[[-4,22],[-146,-26],[48,-10],[-68,46],[86,-94],[0,0],[0,0],[-3,13.5],[-35,-12.5],[0.882,-4.412],[24.446,-18.725],[15.5,8.5],[-14,59.5]],"o":[[12,-66],[187.73,33.431],[-48,10],[68,-46],[-86,94],[0,0],[0,0],[4,-18],[35,12.5],[-2,10],[-23.5,18],[-18.361,-10.069],[6.027,-25.616]],"v":[[470,-288],[690,-442],[780,-298],[776,-410],[900,-290],[785,-81],[700,266],[775.5,-43],[846.5,-86.5],[869.5,-15],[829,93],[775.5,103],[761,16]]}}},{"ty":"st","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":22},"c":{"a":0,"k":[0.9373,0.9059,0.8235]}},{"ty":"fl","hd":true,"c":{"a":0,"k":[1,0,0]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","e":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":6},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[100],"t":70},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":1},"s":[100],"t":112},{"s":[100],"t":178}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":6},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[51.84823072949908],"t":70},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[51.84823072949908],"t":112},{"s":[100],"t":178}]},"m":1}],"ind":1},{"ty":4,"sr":1,"st":16,"op":312,"ip":16,"ln":"19","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[458,-98.5,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[1418,441.5,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"i":[[-18.039,63.738],[-122,-12],[3.5,-29],[30.416,-2.716],[-5,56],[-35.711,12.071],[-15,-74.5],[0,0],[0,0],[-68.501,-17.5],[-10.001,52.5],[0,0],[-36,3.5]],"o":[[30,-106],[278.727,27.416],[-3.5,29],[-28,2.5],[5.001,-56],[35.5,-12],[4.501,31],[0,0],[0,0],[68.499,17.5],[9.724,-51.053],[0,0],[36,-3.5]],"v":[[-2,-178],[220,-354],[482.5,-123],[433.5,-73.5],[363.5,-154],[437,-245],[554,-179.5],[544,-102],[519.001,-1],[560.501,105.5],[691.501,14.5],[740.5,-185],[785.5,-242.5]]}}},{"ty":"st","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":22},"c":{"a":0,"k":[0.9373,0.9059,0.8235]}},{"ty":"fl","hd":true,"c":{"a":0,"k":[1,0,0]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","e":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":16},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[99.89999999999985],"t":80},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[99.89999999999985],"t":122},{"s":[100],"t":182}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":16},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[36.04823072949908],"t":80},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[36.04823072949908],"t":122},{"s":[100],"t":182}]},"m":1}],"ind":2},{"ty":4,"sr":1,"st":8,"op":312,"ip":8,"ln":"18","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-545.843994140625,-58.13648986816406,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[414.156005859375,481.86351013183594,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"i":[[-110.032,257.14],[34,-200],[-44,84],[88,-48],[-7,25],[19.75,3.5],[13.984,-26.752],[4.25,-16],[0.25,-1.75],[-14,-18],[-18.5,17],[-2.25,8],[0,0],[0,0],[-45.5,-4],[-9.833,41.792],[0,0],[0,0],[-52,-7.5],[0,0],[0,0],[0,0],[-33.5,-19],[-10,29.5],[0.5,1.5],[-42.5,-6],[2.5,-20.5],[38.5,4],[1.713,-6.582],[-10,-23.5],[-33.5,-2],[-20.168,30.253],[-22,19],[-2.5,0],[-2.5,35],[-11.5,0],[5,-39.5],[12.5,-0.5],[5,-41],[-24.5,0.5],[2,-52],[-50.5,-1],[-12.5,52.5],[-4,9.5],[-35.5,-8.5],[11,-39],[0,0],[0,0],[-48.133,-6.017],[-5,26.5],[44.517,5.359],[-0.5,0],[55,3.5],[-15,77.5],[-120.055,118.071],[72,-270]],"o":[[184,-430],[-23.343,137.314],[44.583,-85.114],[-112.135,61.165],[7,-25],[-19.75,-3.5],[-11.5,22],[-1.65,6.212],[-0.25,1.75],[14,18],[18.5,-17],[2.25,-8],[0,0],[0,0],[40.991,3.604],[10,-42.5],[0,0],[0,0],[52,7.5],[0,0],[0,0],[0,0],[33.5,19],[10,-29.5],[-0.5,-1.5],[42.5,6],[4.5,-24.5],[-41.469,-4.309],[-9.5,36.5],[9.227,21.682],[33.5,2],[16,-24],[22,-19],[-9,-0.5],[2.5,-35],[11.5,0],[4.5,-38],[-7.511,0.301],[-2.966,24.32],[-15,-2],[-0.663,17.25],[52.028,1.03],[11.91,-50.022],[4,-9.5],[35.5,8.5],[10,-42],[0,0],[0,0],[44,5.5],[-6.5,28],[-54,-6.5],[0.5,0],[-7.418,-0.472],[5.713,-29.519],[121,-119],[-62.967,236.125]],"v":[[-74,-116],[-358,-346],[-198,-236],[-310,-348],[-505,-28.25],[-529.25,-86.75],[-588.75,-61],[-603.5,-16.75],[-617.25,39],[-609.25,93.75],[-550,96],[-522,40],[-492,-83.5],[-522,40.5],[-484,103],[-411.5,20.5],[-384.5,-83],[-411.5,20.5],[-384,109],[-316,20],[-290.5,-83.5],[-316,20.5],[-302,97.5],[-226.5,62],[-199.5,-37.5],[-141,-89],[-104.5,-32],[-141,-88.5],[-199.5,-38.5],[-211.5,66.5],[-151.5,108],[-68.5,62.5],[-39,2.5],[6,-11],[-32,-46],[12,-88],[57,-46],[12,-87.5],[-31.5,-46],[6,-11],[-56,44],[12,108],[102.5,28.5],[125,-49],[187,-88],[214,-12],[253,-178],[202.5,35.5],[238,103],[308,28.5],[238,103],[206,22],[138,109],[102,30],[285,-325],[590,-154]]}}},{"ty":"st","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":22},"c":{"a":0,"k":[0.9373,0.9059,0.8235]}},{"ty":"fl","hd":true,"c":{"a":0,"k":[0.9254,0.0874,0.1797]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","e":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":8},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[88.09999999999995],"t":92},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[88.09999999999995],"t":134},{"s":[100],"t":198}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":8},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[22.348230729499086],"t":98},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[22.348230729499086],"t":146},{"s":[100],"t":198}]},"m":1}],"ind":3},{"ty":4,"sr":1,"st":0,"op":312,"ip":0,"ln":"16","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[960,540]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":false,"i":[[26.05,283.657],[47.14,-113.137],[-66.5,4.5],[-0.5,7.5],[44.5,5.5],[2.5,-25.5],[-15,-42.5],[18.5,-37.5],[42,-1.5],[5.525,27.013],[-37.599,25.225],[5,-46.5],[-295.85,45.395],[72,-176]],"o":[[-36,-392],[-60,144],[42,0],[0.5,-7.5],[-44.5,-5.5],[-2.5,25.5],[15.636,44.303],[-18.5,37.5],[-42,1.5],[-4.5,-22],[39.5,-26.5],[-5.416,50.368],[378,-58],[-63.669,155.635]],"v":[[-420,-170],[-784,-366],[-645.5,-134.5],[-598,-184],[-647,-232.5],[-716.5,-173],[-700,-81.5],[-693,44],[-785.5,103],[-871,37.5],[-838,-55],[-753,-8.5],[-568,234],[372,254]]}}},{"ty":"tm","e":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":0},{"o":{"x":0.4,"y":0.478},"i":{"x":0.74,"y":1},"s":[57.66571166583927],"t":74},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[56.400000000000006],"t":94},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[56.400000000000006],"t":116},{"s":[100],"t":188}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.33,"y":0.52},"i":{"x":0.64,"y":1},"s":[0],"t":0},{"o":{"x":0.4,"y":0.8},"i":{"x":0.74,"y":1},"s":[28.248230729499078],"t":80},{"o":{"x":0.4,"y":0},"i":{"x":0.74,"y":1},"s":[27.699999999999996],"t":100},{"o":{"x":0.36,"y":0},"i":{"x":0.64,"y":0.48},"s":[27.699999999999996],"t":128},{"s":[100],"t":188}]},"m":1},{"ty":"st","lc":2,"lj":2,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":22},"c":{"a":0,"k":[0.9373,0.9059,0.8235]}},{"ty":"fl","hd":true,"c":{"a":0,"k":[0.8495,0.8794,0.9004]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":4},{"ty":4,"sr":1,"st":0,"op":312,"ip":40,"ln":"21","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[883.8469848632812,268.82916259765625,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[960,547.6373596191406,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-9.242,0],[0,7.49],[9.826,0.838],[6.652,3.022],[0,5.611],[-14.93,0],[-9.115,-6.652],[0,0],[8.988,0],[0,-5.255],[-2.006,-1.524],[-11.553,-1.295],[0,-9.572],[17.748,0],[9.826,7.592],[0,0]],"o":[[8.988,7.008],[12.035,0],[0,-5.713],[-10.74,-0.813],[-5.84,-2.793],[0,-9.699],[10.638,0],[0,0],[-7.82,-6.297],[-9.928,0],[0,2.336],[5.383,4.088],[13.203,1.523],[0,10.867],[-12.391,0],[0,0],[0,0]],"v":[[-25.771,15.107],[2.387,25.746],[22.014,12.543],[7.287,1.32],[-18.636,-2.641],[-27.624,-15.615],[-3.224,-33.008],[26.914,-22.725],[22.369,-16.174],[-3.097,-25.645],[-19.449,-15.946],[-16.402,-10.334],[9.522,-5.891],[30.316,12.796],[2.514,33.007],[-30.316,21.911],[-25.771,15.132]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1623.3671875,477.15692138671875],"t":60,"ti":[0,0],"to":[-29.667,0]},{"o":{"x":0.248,"y":0.248},"i":{"x":0.833,"y":0.833},"s":[1445.3671875,477.15692138671875],"t":110,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1445.3671875,477.15692138671875],"t":120,"ti":[-29.667,0],"to":[29.667,0]},{"s":[1623.3671875,477.15692138671875],"t":170,"ti":[29.667,0],"to":[29.667,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":60},{"o":{"x":0.17,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":110},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":120},{"s":[0],"t":170}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-26.063,-31.459],[23.474,-31.459],[23.474,-24.223],[-18.217,-24.223],[-18.217,-5.764],[7.021,-5.764],[7.021,1.472],[-18.217,1.472],[-18.217,24.247],[26.063,24.247],[26.063,31.484],[-26.038,31.484],[-26.038,-31.484]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1634.3865966796875,477.0050048828125],"t":56,"ti":[0,0],"to":[-46.333,0]},{"o":{"x":0.238,"y":0.238},"i":{"x":0.833,"y":0.833},"s":[1356.3865966796875,477.0050048828125],"t":106,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1356.3865966796875,477.0050048828125],"t":124,"ti":[-46.333,0],"to":[46.333,0]},{"s":[1634.3865966796875,477.0050048828125],"t":174,"ti":[46.333,0],"to":[46.333,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":56},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":106},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":124},{"s":[0],"t":174}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-3.974,-31.776],[3.974,-31.776],[3.974,31.776],[-3.974,31.776]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1620.571533203125,476.966796875],"t":52,"ti":[0,0],"to":[-56.667,0]},{"o":{"x":0.231,"y":0.231},"i":{"x":0.833,"y":0.833},"s":[1280.571533203125,476.966796875],"t":102,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1280.571533203125,476.966796875],"t":128,"ti":[-56.667,0],"to":[56.667,0]},{"s":[1620.571533203125,476.966796875],"t":178,"ti":[56.667,0],"to":[56.667,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":52},{"o":{"x":0.164,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":102},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":128},{"s":[0],"t":178}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[12.746,0],[0,19.143],[-18.789,0],[-5.967,-9.344],[0,0],[9.116,0],[0,-14.956],[-14.015,0],[-5.027,9.472]],"o":[[-5.84,10.969],[-18.815,0],[0,-19.145],[11.679,0],[0,0],[-3.961,-7.593],[-14.015,0],[0,14.954],[9.801,0],[0,0]],"v":[[30.773,15.932],[1.803,32.995],[-30.773,0.064],[1.803,-32.995],[29.377,-18.268],[22.826,-13.596],[1.802,-25.632],[-22.725,0.064],[1.802,25.631],[24.476,11.26]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1639.7939453125,477.1443176269531],"t":48,"ti":[0,0],"to":[-72.5,0]},{"o":{"x":0.224,"y":0.224},"i":{"x":0.833,"y":0.833},"s":[1204.7939453125,477.1443176269531],"t":98,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1204.7939453125,477.1443176269531],"t":132,"ti":[-72.5,0],"to":[72.5,0]},{"s":[1639.7939453125,477.1443176269531],"t":182,"ti":[72.5,0],"to":[72.5,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":48},{"o":{"x":0.163,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":98},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":132},{"s":[0],"t":182}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-14.371,0],[0,13.102],[0,0],[0,0],[0,0],[18.561,0],[0,15.438],[0,0],[0,0]],"o":[[0,13.076],[14.37,0],[0,0],[0,0],[0,0],[0,15.412],[-18.561,0],[0,0],[0,0],[0,0]],"v":[[-18.104,8.48],[0,24.958],[18.104,8.48],[18.104,-32.297],[25.923,-32.297],[25.923,10.562],[0,32.297],[-25.923,10.562],[-25.923,-32.297],[-18.104,-32.297]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1629.8438720703125,477.8424987792969],"t":46,"ti":[0,0],"to":[-86.833,0]},{"o":{"x":0.222,"y":0.222},"i":{"x":0.833,"y":0.833},"s":[1108.8438720703125,477.8424987792969],"t":96,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1108.8438720703125,477.8424987792969],"t":134,"ti":[-86.833,0],"to":[86.833,0]},{"s":[1629.8438720703125,477.8424987792969],"t":184,"ti":[86.833,0],"to":[86.833,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":46},{"o":{"x":0.162,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":96},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":134},{"s":[0],"t":184}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-24.451,-31.459],[-16.631,-31.459],[-16.631,24.247],[24.476,24.247],[24.476,31.484],[-24.476,31.484],[-24.476,-31.484]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1637.7099609375,477.0050048828125],"t":44,"ti":[0,0],"to":[-102.333,0]},{"o":{"x":0.219,"y":0.219},"i":{"x":0.833,"y":0.833},"s":[1023.7099609375,477.0050048828125],"t":94,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1023.7099609375,477.0050048828125],"t":136,"ti":[-102.333,0],"to":[102.333,0]},{"s":[1637.7099609375,477.0050048828125],"t":186,"ti":[102.333,0],"to":[102.333,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":44},{"o":{"x":0.161,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":94},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":136},{"s":[0],"t":186}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-3.961,-0.051],[-32.347,-31.484],[-22.547,-31.484],[0,-6.602],[22.419,-31.484],[32.347,-31.484],[3.834,0.051],[3.834,31.484],[-3.986,31.484],[-3.986,-0.051]]}}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1635.6060791015625,477.0303039550781],"t":42,"ti":[0,0],"to":[-127.5,0]},{"o":{"x":0.217,"y":0.217},"i":{"x":0.833,"y":0.833},"s":[870.6060791015625,477.0303039550781],"t":92,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[870.6060791015625,477.0303039550781],"t":138,"ti":[-127.5,0],"to":[127.5,0]},{"s":[1635.6060791015625,477.0303039550781],"t":188,"ti":[127.5,0],"to":[127.5,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":42},{"o":{"x":0.161,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":92},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":138},{"s":[0],"t":188}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,7.134],[7.338,0],[0,0],[0,0],[0,0]],"o":[[7.363,0],[0,-7.135],[0,0],[0,0],[0,0],[0,0]],"v":[[10.499,24.299],[22.052,12.493],[10.499,0.686],[-22.432,0.686],[-22.432,24.274],[10.499,24.274]]}}},{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,5.611],[6.779,0],[0,0],[0,0]],"o":[[6.652,0],[0,-5.967],[0,0],[0,0],[0,0]],"v":[[7.706,-6.55],[17.862,-15.31],[7.706,-24.197],[-22.432,-24.197],[-22.432,-6.55]]}}},{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,-10.156],[3.732,-2.437],[0,-7.237],[13.304,0],[0,0],[0,0]],"o":[[0,0],[12.619,0],[0,5.027],[6.779,3.86],[0,11.451],[0,0],[0,0],[0,0]],"v":[[-30.253,-31.433],[6.995,-31.433],[25.809,-15.792],[20.097,-4.24],[30.253,12.569],[9.001,31.484],[-30.252,31.484],[-30.252,-31.484]]}}},{"ty":"mm","mm":1},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[1633.89501953125,476.9788818359375],"t":40,"ti":[0,0],"to":[-141.833,0]},{"o":{"x":0.215,"y":0.215},"i":{"x":0.833,"y":0.833},"s":[782.89501953125,476.9788818359375],"t":90,"ti":[0,0],"to":[0,0]},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[782.89501953125,476.9788818359375],"t":140,"ti":[-141.833,0],"to":[141.833,0]},{"s":[1633.89501953125,476.9788818359375],"t":190,"ti":[141.833,0],"to":[141.833,0]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[0],"t":40},{"o":{"x":0.16,"y":0},"i":{"x":0.833,"y":1},"s":[100],"t":90},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[100],"t":140},{"s":[0],"t":190}]}}]},{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":1,"k":[{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[1199.872,-2.909],[1201.431,-2.909],[1201.431,4.708],[1199.872,4.708]]}],"t":36},{"o":{"x":0.76,"y":0},"i":{"x":0.24,"y":1},"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-273.708,-3.809],[273.708,-3.809],[273.708,3.809],[-273.708,3.809]]}],"t":92},{"o":{"x":0.76,"y":0},"i":{"x":0.833,"y":1},"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-273.708,-3.809],[273.708,-3.809],[273.708,3.809],[-273.708,3.809]]}],"t":104},{"s":[{"c":true,"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[365.116,-3.85],[365.075,-4.663],[365.075,2.954],[365.116,3.767]]}],"t":150}]}},{"ty":"fl","c":{"a":0,"k":[0.9373,0.9059,0.8235]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[391.5409851074219,477.1573181152344]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":5},{"ty":2,"sr":1,"st":0,"op":312,"ip":0,"hd":true,"ln":"15","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[883.8469848632812,268.82916259765625]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[960,547.6373596191406,0]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"refId":"1","ind":6}],"v":"5.7.0","fr":60,"op":202,"ip":0,"assets":[{"id":"1","e":0,"w":1768,"h":538,"p":"/Users/mauali/Library/Application Support/LottieFiles for After Effects/images/15-Layer 1.png","u":""}]} \ No newline at end of file