diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index afbdd468..c52b87a0 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -27,3 +27,5 @@ jobs: env: APPLE_MACOSX_SDK: MacOSX13 run: make demo-player + - name: Run Tests + run: make test diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..b130505c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +resolver = "2" + +members = [ + "demo-player", + "dotlottie-rs", + "dotlottie-fms", + "dotlottie-ffi" +] \ No newline at end of file diff --git a/Makefile b/Makefile index 67264ea3..01d571c2 100644 --- a/Makefile +++ b/Makefile @@ -923,6 +923,14 @@ mac-setup: export UNIFFI_BINDGEN_CPP_VERSION:= $(UNIFFI_BINDGEN_CPP_VERSION) mac-setup: @./.$@.sh +.PHONY: test +test: test-all + +.PHONY: test-all +test-all: + $(info $(YELLOW)Running tests for workspace$(NC)) + cargo test -- --test-threads=1 + .PHONY: help help: @echo "Welcome to the $(GREEN)dotlottie-player$(NC) build system!" @@ -962,5 +970,6 @@ help: @echo " - $(YELLOW)clean-deps$(NC) - clean up all native dependency builds & artifacts" @echo " - $(YELLOW)clean-build$(NC) - clean up any extraneous build files (useful for ensuring a clean working directory)" @echo " - $(YELLOW)distclean$(NC) - clean up everything" + @echo " - $(YELLOW)test$(NC) - run all tests" @echo @echo diff --git a/dotlottie-fms/src/dolottie_manager.rs b/dotlottie-fms/src/dolottie_manager.rs index 8aac985b..b629ff7f 100644 --- a/dotlottie-fms/src/dolottie_manager.rs +++ b/dotlottie-fms/src/dolottie_manager.rs @@ -52,9 +52,9 @@ impl DotLottieManager { } } - pub fn init(&mut self, dotlottie: Vec) -> Result { + pub fn init(&mut self, dotlottie: &[u8]) -> Result { // Initialize the manager with the dotLottie file - let manifest = get_manifest(&dotlottie); + let manifest = get_manifest(dotlottie); match manifest { Ok(manifest) => { @@ -70,7 +70,7 @@ impl DotLottieManager { self.active_animation_id = id; self.manifest = manifest; - self.zip_data = dotlottie; + self.zip_data = dotlottie.to_vec(); return Ok(true); } diff --git a/dotlottie-fms/src/functions.rs b/dotlottie-fms/src/functions.rs index ad38f649..3f1aabd7 100644 --- a/dotlottie-fms/src/functions.rs +++ b/dotlottie-fms/src/functions.rs @@ -121,7 +121,7 @@ pub fn get_animations(bytes: &Vec) -> Result, DotLot /// /// bytes: The bytes of the dotLottie file /// Result: The extracted manifest, or an error -pub fn get_manifest(bytes: &Vec) -> Result { +pub fn get_manifest(bytes: &[u8]) -> Result { let mut archive = ZipArchive::new(io::Cursor::new(bytes)).map_err(|_| DotLottieError::ArchiveOpenError)?; diff --git a/dotlottie-rs/src/dotlottie_player.rs b/dotlottie-rs/src/dotlottie_player.rs index 6c89fd28..5f4266bd 100644 --- a/dotlottie-rs/src/dotlottie_player.rs +++ b/dotlottie-rs/src/dotlottie_player.rs @@ -31,7 +31,7 @@ pub enum PlaybackState { Stopped, } -#[derive(Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Mode { Forward, Reverse, @@ -67,6 +67,22 @@ pub struct Config { pub marker: String, } +impl Default for Config { + fn default() -> Self { + Config { + mode: Mode::Forward, + loop_animation: false, + speed: 1.0, + use_frame_interpolation: true, + autoplay: false, + segment: vec![], + background_color: 0x00000000, + layout: Layout::default(), + marker: String::new(), + } + } +} + struct DotLottieRuntime { renderer: LottieRenderer, playback_state: PlaybackState, @@ -587,10 +603,6 @@ impl DotLottieRuntime { } } - if self.config.autoplay && loaded { - self.play(); - } - loaded } @@ -613,8 +625,8 @@ impl DotLottieRuntime { } } - pub fn load_dotlottie_data(&mut self, file_data: &Vec, width: u32, height: u32) -> bool { - if self.dotlottie_manager.init(file_data.clone()).is_err() { + pub fn load_dotlottie_data(&mut self, file_data: &[u8], width: u32, height: u32) -> bool { + if self.dotlottie_manager.init(file_data).is_err() { return false; } @@ -768,6 +780,10 @@ impl DotLottiePlayer { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load(); }); + + if self.config().autoplay { + self.play(); + } } else { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load_error(); @@ -789,6 +805,10 @@ impl DotLottiePlayer { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load(); }); + + if self.config().autoplay { + self.play(); + } } else { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load_error(); @@ -800,7 +820,7 @@ impl DotLottiePlayer { is_ok } - pub fn load_dotlottie_data(&self, file_data: &Vec, width: u32, height: u32) -> bool { + pub fn load_dotlottie_data(&self, file_data: &[u8], width: u32, height: u32) -> bool { let is_ok = self .runtime .write() @@ -810,6 +830,10 @@ impl DotLottiePlayer { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load(); }); + + if self.config().autoplay { + self.play(); + } } else { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load_error(); @@ -831,6 +855,10 @@ impl DotLottiePlayer { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load(); }); + + if self.config().autoplay { + self.play(); + } } else { self.observers.read().unwrap().iter().for_each(|observer| { observer.on_load_error(); diff --git a/dotlottie-rs/src/lottie_renderer/mod.rs b/dotlottie-rs/src/lottie_renderer/mod.rs index 259be8e1..7df05343 100644 --- a/dotlottie-rs/src/lottie_renderer/mod.rs +++ b/dotlottie-rs/src/lottie_renderer/mod.rs @@ -1,7 +1,5 @@ use thiserror::Error; -mod tests; - use crate::{Animation, Canvas, Layout, Shape, TvgColorspace, TvgEngine, TvgError}; #[derive(Error, Debug)] diff --git a/dotlottie-rs/src/lottie_renderer/tests.rs b/dotlottie-rs/src/lottie_renderer/tests.rs deleted file mode 100644 index b7ec905a..00000000 --- a/dotlottie-rs/src/lottie_renderer/tests.rs +++ /dev/null @@ -1,167 +0,0 @@ -#[cfg(test)] -mod tests { - - use crate::lottie_renderer::LottieRenderer; - use crate::lottie_renderer::LottieRendererError; - - #[test] - fn test_new_lottie_renderer() { - let renderer = LottieRenderer::new(); - - assert_eq!(renderer.width, 0); - assert_eq!(renderer.height, 0); - - assert_eq!(renderer.buffer.len(), 0); - } - - #[test] - fn test_load_data() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - } - - #[test] - fn test_total_frames() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.total_frames(); - - assert!(result.is_ok()); - - let total_frames = result.unwrap(); - - assert!(total_frames > 0.0); - } - - #[test] - fn test_duration() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.duration(); - - assert!(result.is_ok()); - - let duration = result.unwrap(); - - assert!(duration > 0.0); - } - - #[test] - fn test_current_frame() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.current_frame(); - - assert!(result.is_ok()); - - let current_frame = result.unwrap(); - - assert_eq!(current_frame, 0.0); - } - - #[test] - fn test_clear() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - renderer.clear(); - - assert_eq!(renderer.buffer.len(), 0); - } - - #[test] - fn test_render() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.render(); - - assert!(result.is_ok()); - - let buffer = renderer.buffer; - - assert!(buffer.len() > 0); - - let mut has_non_zero = false; - - for pixel in buffer { - if pixel != 0 { - has_non_zero = true; - break; - } - } - - assert!(has_non_zero); - } - - #[test] - fn test_set_frame() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.set_frame(10.0); - - assert!(result.is_ok()); - - let result = renderer.current_frame(); - - assert!(result.is_ok()); - - let current_frame = result.unwrap(); - - assert_eq!(current_frame, 10.0); - } - - #[test] - fn test_resize() { - let mut renderer = LottieRenderer::new(); - - let lottie_data = include_str!("./fixtures/lottie.json"); - - let result = renderer.load_data(lottie_data, 100, 100, true); - - assert!(result.is_ok()); - - let result = renderer.resize(400, 400); - - assert!(result.is_ok()); - } -} diff --git a/dotlottie-rs/tests/assets/emoji.lottie b/dotlottie-rs/tests/assets/emoji.lottie new file mode 100644 index 00000000..5ec76a79 Binary files /dev/null and b/dotlottie-rs/tests/assets/emoji.lottie differ diff --git a/dotlottie-rs/tests/assets/test.json b/dotlottie-rs/tests/assets/test.json new file mode 100644 index 00000000..dbab39aa --- /dev/null +++ b/dotlottie-rs/tests/assets/test.json @@ -0,0 +1,2086 @@ +{ + "v": "5.7.0", + "ip": 0, + "op": 42, + "fr": 30, + "w": 1500, + "h": 1500, + "nm": "B", + "assets": [], + "markers": [ + { + "cm": "Marker_1", + "tm": 0, + "dr": 10 + }, + { + "cm": "Marker_2", + "tm": 10, + "dr": 10 + }, + { + "cm": "Marker_3", + "tm": 20, + "dr": 10 + }, + { + "cm": "Marker_4", + "tm": 30, + "dr": 12 + } + ], + "layers": [ + { + "ind": 1, + "ty": 4, + "nm": "R", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100] + }, + { + "t": 42, + "s": [100] + } + ] + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [0] + }, + { + "t": 42, + "s": [0] + } + ] + }, + "p": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [750, 953] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [753, 1057] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [757, 877] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [757, 1013] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [743, 913] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [750, 985] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [750, 945] + }, + { + "t": 42, + "s": [750, 953] + } + ], + "a": 1 + }, + "a": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [190, 169] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [333, 65] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [117, 245] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [243, 109] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [155, 209] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [190, 137] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [190, 177] + }, + { + "t": 42, + "s": [190, 169] + } + ], + "a": 1 + }, + "s": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100, 100] + }, + { + "t": 42, + "s": [100, 100] + } + ], + "a": 1 + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "shapes": [ + { + "ty": "sh", + "nm": "", + "bm": 0, + "d": 1, + "ks": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [300, 0], + [380, 80], + [380, 258], + [300, 338], + [80, 338], + [0, 258] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [ + { + "i": [ + [0, 0], + [-35.87, 0], + [0, 0], + [0, -35.87], + [0, 0], + [35.87, 0], + [0, 0], + [0, 35.87] + ], + "o": [ + [0, -35.87], + [0, 0], + [35.87, 0], + [0, 0], + [0, 35.87], + [0, 0], + [-35.87, 0], + [0, 0] + ], + "v": [ + [0, 65], + [65, 0], + [601, 0], + [666, 65], + [666, 65], + [601, 130], + [65, 130], + [0, 65] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [154, 0], + [234, 80], + [234, 410], + [154, 490], + [80, 490], + [0, 410] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [406, 0], + [486, 80], + [486, 138], + [406, 218], + [80, 218], + [0, 138] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [230, 0], + [310, 80], + [310, 338], + [230, 418], + [80, 418], + [0, 338] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [300, 0], + [380, 80], + [380, 194], + [300, 274], + [80, 274], + [0, 194] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [300, 0], + [380, 80], + [380, 274], + [300, 354], + [80, 354], + [0, 274] + ], + "c": true + } + ] + }, + { + "t": 42, + "s": [ + { + "i": [ + [0, 0], + [-44.15, 0], + [0, 0], + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15] + ], + "o": [ + [0, -44.15], + [0, 0], + [44.15, 0], + [0, 0], + [0, 44.15], + [0, 0], + [-44.15, 0], + [0, 0] + ], + "v": [ + [0, 80], + [80, 0], + [300, 0], + [380, 80], + [380, 258], + [300, 338], + [80, 338], + [0, 258] + ], + "c": true + } + ] + } + ], + "a": 1 + } + }, + { + "ty": "fl", + "nm": "", + "bm": 0, + "c": { + "sid": "c1", + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [0.271, 0.271, 0.271] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [0.271, 0.271, 0.271] + }, + { + "t": 42, + "s": [0.271, 0.271, 0.271] + } + ] + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100] + }, + { + "t": 42, + "s": [100] + } + ] + }, + "r": 1 + } + ], + "hasMask": false, + "ip": 0, + "op": 43, + "st": 0, + "hd": false + }, + { + "ind": 2, + "ty": 4, + "nm": "E", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100] + }, + { + "t": 42, + "s": [100] + } + ] + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [0] + }, + { + "t": 42, + "s": [0] + } + ] + }, + "p": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [750, 594] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [750, 802] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [750, 226] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [750, 664] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [750, 318] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [750, 612] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [750, 514] + }, + { + "t": 42, + "s": [750, 594] + } + ], + "a": 1 + }, + "a": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [190, 190] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [190, 190] + }, + { + "t": 42, + "s": [190, 190] + } + ], + "a": 1 + }, + "s": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100, 100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100, 100] + }, + { + "t": 42, + "s": [100, 100] + } + ], + "a": 1 + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "shapes": [ + { + "ty": "sh", + "nm": "", + "bm": 0, + "d": 1, + "ks": { + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + }, + { + "t": 42, + "s": [ + { + "i": [ + [-104.86, 0], + [0, -104.86], + [104.86, 0], + [0, 104.86] + ], + "o": [ + [104.86, 0], + [0, 104.86], + [-104.86, 0], + [0, -104.86] + ], + "v": [ + [190, 0], + [380, 190], + [190, 380], + [0, 190] + ], + "c": true + } + ] + } + ], + "a": 1 + } + }, + { + "ty": "fl", + "nm": "", + "bm": 0, + "c": { + "sid": "c0", + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [1, 0.377, 0] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [1, 0.377, 0] + }, + { + "t": 42, + "s": [1, 0.377, 0] + } + ] + }, + "o": { + "a": 1, + "k": [ + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 0, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 6, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 12, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 18, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 24, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 30, + "s": [100] + }, + { + "i": { + "x": 0.515, + "y": 0.955 + }, + "o": { + "x": 0.455, + "y": 0.03 + }, + "t": 36, + "s": [100] + }, + { + "t": 42, + "s": [100] + } + ] + }, + "r": 1 + } + ], + "hasMask": false, + "ip": 0, + "op": 43, + "st": 0, + "hd": false + }, + { + "ind": 3, + "ty": 1, + "nm": "B", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100 + }, + "r": { + "a": 0, + "k": 0 + }, + "p": { + "a": 0, + "k": [0, 0] + }, + "a": { + "a": 0, + "k": [0, 0] + }, + "s": { + "a": 0, + "k": [100, 100] + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + } + }, + "hasMask": false, + "ip": 0, + "op": 43, + "st": 0, + "hd": false, + "sc": "#ffffff", + "sh": 1500, + "sw": 1500 + } + ] +} diff --git a/dotlottie-rs/tests/assets/test.lottie b/dotlottie-rs/tests/assets/test.lottie new file mode 100644 index 00000000..bd94a85a Binary files /dev/null and b/dotlottie-rs/tests/assets/test.lottie differ diff --git a/dotlottie-rs/tests/autoplay.rs b/dotlottie-rs/tests/autoplay.rs new file mode 100644 index 00000000..ea661ae5 --- /dev/null +++ b/dotlottie-rs/tests/autoplay.rs @@ -0,0 +1,83 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_default_autoplay() { + let player = DotLottiePlayer::new(Config::default()); + + assert!(!player.config().autoplay); + } + + #[test] + fn test_set_autoplay() { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.autoplay = true; + player.set_config(config); + + assert!(player.config().autoplay); + } + + #[test] + fn test_autoplay() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!(player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT)); + assert!(player.is_playing()); + assert!(!player.is_paused()); + assert!(!player.is_stopped()); + assert!(!player.is_complete()); + assert_eq!(player.current_frame(), 0.0); + + let mut rendered_frames: Vec = vec![]; + + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!(!rendered_frames.is_empty()); + println!("Rendered frames: {:?}", rendered_frames.len()); + } + + #[test] + fn test_no_autoplay() { + let player = DotLottiePlayer::new(Config { + autoplay: false, + ..Config::default() + }); + + let loaded = player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT); + + assert!(loaded); + + assert!(!player.is_playing()); + assert!(!player.is_paused()); + assert!(player.is_stopped()); + assert!(!player.is_complete()); + assert!(player.current_frame() == 0.0); + + let times: usize = 10; + + for _ in 0..times { + let next_frame = player.request_frame(); + assert_eq!(next_frame, 0.0); + } + } +} diff --git a/dotlottie-rs/tests/events.rs b/dotlottie-rs/tests/events.rs new file mode 100644 index 00000000..0e7f7e79 --- /dev/null +++ b/dotlottie-rs/tests/events.rs @@ -0,0 +1,158 @@ +use std::sync::{Arc, Mutex}; + +use dotlottie_player_core::{Config, DotLottiePlayer, Observer}; + +mod test_utils; + +use crate::test_utils::{HEIGHT, WIDTH}; + +struct MockObserver { + events: Arc>>, +} + +impl MockObserver { + fn new(events: Arc>>) -> Self { + MockObserver { events } + } +} + +impl Observer for MockObserver { + fn on_load_error(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_load_error".to_string()); + } + + fn on_load(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_load".to_string()); + } + + fn on_play(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_play".to_string()); + } + + fn on_pause(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_pause".to_string()); + } + + fn on_stop(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_stop".to_string()); + } + + fn on_complete(&self) { + let mut events = self.events.lock().unwrap(); + events.push("on_complete".to_string()); + } + + fn on_loop(&self, loop_count: u32) { + let mut events = self.events.lock().unwrap(); + events.push(format!("on_loop: {}", loop_count)); + } + + fn on_frame(&self, frame: f32) { + let mut events = self.events.lock().unwrap(); + events.push(format!("on_frame: {}", frame)); + } + + fn on_render(&self, frame: f32) { + let mut events = self.events.lock().unwrap(); + events.push(format!("on_render: {}", frame)); + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_subscribe_unsubscribe() { + let player = + DotLottiePlayer::new(Config { + autoplay: true, + loop_animation: true, + use_frame_interpolation: false, + ..Config::default() + }); + + let events = Arc::new(Mutex::new(vec![])); + let observer_events = Arc::clone(&events); + + let observer = MockObserver::new(observer_events); + let observer_arc: Arc = Arc::new(observer); + player.subscribe(Arc::clone(&observer_arc)); + + assert!( + !player.load_animation_path("invalid/path", WIDTH, HEIGHT), + "Invalid path should not load" + ); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Valid path should load" + ); + + let mut expected_events = vec![ + "on_load_error".to_string(), + "on_load".to_string(), + "on_play".to_string(), + ]; + + // animation loop + loop { + let next_frame = player.request_frame(); + if player.set_frame(next_frame) { + expected_events.push(format!("on_frame: {}", player.current_frame())); + if player.render() { + expected_events.push(format!("on_render: {}", player.current_frame())); + if player.is_complete() { + if player.config().loop_animation { + let loop_count = player.loop_count(); + expected_events.push(format!("on_loop: {}", loop_count)); + + if loop_count == 1 { + player.pause(); + break; + } + } else { + expected_events.push("on_complete".to_string()); + break; + } + } + } + } + } + + player.stop(); + + expected_events.push("on_pause".to_string()); + expected_events.push("on_stop".to_string()); + + let recorded_events = events.lock().unwrap(); + + for (i, event) in recorded_events.iter().enumerate() { + assert_eq!( + event, &expected_events[i], + "Mismatch at event index {}: expected '{}', found '{}'", + i, expected_events[i], event + ); + } + + // unsubscribe the observer + player.unsubscribe(&observer_arc); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Valid path should load" + ); + + assert_eq!( + recorded_events.len(), + expected_events.len(), + "Events should not change after unsubscribing" + ); + } +} diff --git a/dotlottie-rs/tests/frame_interpolation.rs b/dotlottie-rs/tests/frame_interpolation.rs new file mode 100644 index 00000000..815ddbb7 --- /dev/null +++ b/dotlottie-rs/tests/frame_interpolation.rs @@ -0,0 +1,64 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_default_use_frame_interpolation() { + let player = DotLottiePlayer::new(Config::default()); + + assert!(player.config().use_frame_interpolation); + } + + #[test] + fn test_set_use_frame_interpolation() { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.use_frame_interpolation = false; + player.set_config(config); + + assert!(!player.config().use_frame_interpolation); + } + + #[test] + fn test_disable_frame_interpolation() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!(player.load_dotlottie_data(include_bytes!("assets/emoji.lottie"), WIDTH, HEIGHT)); + + let total_frames = player.total_frames(); + let mut rendered_frames: Vec = vec![]; + + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!(!rendered_frames.is_empty()); + assert_eq!({ rendered_frames.len() }, total_frames as usize); + + assert_eq!(rendered_frames[rendered_frames.len() - 1], total_frames); + + for i in 0..rendered_frames.len() { + assert!( + rendered_frames[i] == rendered_frames[i].floor(), + "Frame {} is interpolated.", + i + ); + } + } +} diff --git a/dotlottie-rs/tests/markers.rs b/dotlottie-rs/tests/markers.rs new file mode 100644 index 00000000..6278d3f9 --- /dev/null +++ b/dotlottie-rs/tests/markers.rs @@ -0,0 +1,133 @@ +use dotlottie_player_core::{Config, DotLottiePlayer, Marker}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_default_marker() { + let player = DotLottiePlayer::new(Config::default()); + + assert!( + player.config().marker.is_empty(), + "Expected no marker by default" + ); + } + + #[test] + fn test_markers() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!( + player.markers().is_empty(), + "Expected no markers before loading animation" + ); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + let actual_markers = player.markers(); + + assert_eq!(actual_markers.len(), 4); + + let expected_markers = vec![ + Marker { + name: "Marker_1".to_string(), + time: 0.0, + duration: 10.0, + }, + Marker { + name: "Marker_2".to_string(), + time: 10.0, + duration: 10.0, + }, + Marker { + name: "Marker_3".to_string(), + time: 20.0, + duration: 10.0, + }, + Marker { + name: "Marker_4".to_string(), + time: 30.0, + duration: 12.0, + }, + ]; + + for marker in actual_markers { + let expected = expected_markers + .iter() + .find(|m| m.name == marker.name) + .unwrap(); + + assert_eq!(marker.name, expected.name, "Expected marker name to match"); + assert_eq!(marker.time, expected.time, "Expected marker time to match"); + assert_eq!( + marker.duration, expected.duration, + "Expected marker duration to match" + ); + } + } + + #[test] + fn test_set_marker() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + let marker_name = "Marker_3".to_string(); + + player.set_config(Config { + marker: marker_name.clone(), + ..player.config() + }); + + assert_eq!(player.config().marker, marker_name.clone()); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + assert!(player.is_playing(), "Animation should be playing"); + + // assert current frame is the marker time + assert_eq!(player.current_frame(), 20.0); + + let mut rendered_frames: Vec = vec![]; + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + // assert if all rendered frames are within the marker time and time+duration and in increasing order, as the mode is forward + let marker = player + .markers() + .into_iter() + .find(|m| m.name == marker_name.clone()) + .unwrap(); + + for frame in rendered_frames { + assert!( + frame >= marker.time && frame <= marker.time + marker.duration, + "Expected frame to be within marker time and time+duration" + ); + } + } +} diff --git a/dotlottie-rs/tests/multi_animation.rs b/dotlottie-rs/tests/multi_animation.rs new file mode 100644 index 00000000..971dfda3 --- /dev/null +++ b/dotlottie-rs/tests/multi_animation.rs @@ -0,0 +1,39 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_load_animation() { + let player = DotLottiePlayer::new(Config::default()); + assert!(player.load_dotlottie_data(include_bytes!("assets/emoji.lottie"), WIDTH, HEIGHT)); + + let manifest = player.manifest(); + + assert!(manifest.is_some(), "Manifest is not loaded"); + + let manifest = manifest.unwrap(); + + let animations = manifest.animations; + + for animation in animations { + assert!( + player.load_animation(&animation.id, WIDTH, HEIGHT), + "Failed to load animation with id {}", + animation.id + ); + + /* + TODO: assert if the currently loaded animation is the same as the one we loaded + require the player to have a method to get the currently loaded animation + + let current_animation = player.current_animation(); + assert_eq!(current_animation.id, animation.id); + */ + } + } +} diff --git a/dotlottie-rs/tests/play_mode.rs b/dotlottie-rs/tests/play_mode.rs new file mode 100644 index 00000000..218152d3 --- /dev/null +++ b/dotlottie-rs/tests/play_mode.rs @@ -0,0 +1,294 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; + +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod play_mode_tests { + use dotlottie_player_core::Mode; + + use super::*; + + #[test] + fn test_default_play_mode() { + let player = DotLottiePlayer::new(Config::default()); + + assert_eq!(player.config().mode, Mode::Forward); + } + + #[test] + fn test_set_play_mode() { + let play_modes = vec![ + Mode::Forward, + Mode::Reverse, + Mode::Bounce, + Mode::ReverseBounce, + ]; + + for mode in play_modes { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.mode = mode; + player.set_config(config); + + assert_eq!( + player.config().mode, + mode, + "Expected play mode to be {:?}, found {:?}", + mode, + player.config().mode + ); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + match mode { + Mode::Forward => { + assert_eq!( + player.current_frame(), + 0.0, + "Expected current frame to be 0" + ); + } + Mode::Reverse => { + assert_eq!( + player.current_frame(), + player.total_frames(), + "Expected current frame to be total frames" + ); + } + Mode::Bounce => { + assert_eq!( + player.current_frame(), + 0.0, + "Expected current frame to be 0" + ); + } + Mode::ReverseBounce => { + assert_eq!( + player.current_frame(), + player.total_frames(), + "Expected current frame to be total frames" + ); + } + } + } + } + + #[test] + fn test_forward_play_mode() { + let player = + DotLottiePlayer::new(Config { + mode: Mode::Forward, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + assert!(player.is_playing(), "Animation should be playing"); + assert!(!player.is_complete(), "Animation should not be complete"); + + let mut rendered_frames: Vec = vec![]; + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!( + rendered_frames.len() >= player.total_frames() as usize, + "Expected rendered frames to be greater than or equal to total frames" + ); + + let mut prev_frame = 0.0; + for frame in rendered_frames { + assert!( + frame >= prev_frame, + "Expected frame to be greater than or equal to previous frame" + ); + prev_frame = frame; + } + + assert_eq!( + player.total_frames(), + prev_frame, + "Expected last frame to be total frames" + ); + } + + #[test] + fn test_reverse_play_mode() { + let player = + DotLottiePlayer::new(Config { + mode: Mode::Reverse, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + assert!(player.is_playing(), "Animation should be playing"); + assert!(!player.is_complete(), "Animation should not be complete"); + + let mut rendered_frames: Vec = vec![]; + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!( + rendered_frames.len() >= player.total_frames() as usize, + "Expected rendered frames to be greater than or equal to total frames" + ); + + let mut prev_frame = player.total_frames(); + for frame in rendered_frames { + assert!( + frame <= prev_frame, + "Expected frame to be less than or equal to previous frame" + ); + prev_frame = frame; + } + + // check if the last frame is 0 + assert_eq!(0.0, prev_frame, "Expected last frame to be 0"); + } + + #[test] + #[ignore = "fail cause of a bug in is_complete()"] + fn test_bounce_play_mode() { + let player = + DotLottiePlayer::new(Config { + mode: Mode::Bounce, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + let mut rendered_frames: Vec = vec![]; + + assert!(player.is_playing(), "Animation should be playing"); + assert!(!player.is_complete(), "Animation should not be complete"); + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!( + rendered_frames.len() >= player.total_frames() as usize, + "Expected rendered frames to be greater than or equal to total frames" + ); + + // check if the rendered frames are increasing and decreasing + let mut prev_frame = 0.0; + for frame in &rendered_frames { + assert!( + frame >= &prev_frame, + "Expected frame to be greater than or equal to previous frame" + ); + prev_frame = *frame; + } + + for frame in rendered_frames.iter().rev() { + assert!( + frame <= &prev_frame, + "Expected frame to be less than or equal to previous frame" + ); + prev_frame = *frame; + } + + // check if the last frame is 0 + assert_eq!(0.0, prev_frame, "Expected last frame to be 0"); + } + + #[test] + #[ignore = "fail cause of a bug in is_complete()"] + fn test_reverse_bounce_play_mode() { + let player = + DotLottiePlayer::new(Config { + mode: Mode::ReverseBounce, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + assert!(player.is_playing(), "Animation should be playing"); + + let mut rendered_frames: Vec = vec![]; + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + + if player.set_frame(next_frame) && player.render() { + let current_frame = player.current_frame(); + rendered_frames.push(current_frame); + } + } + + assert!( + rendered_frames.len() >= player.total_frames() as usize, + "Expected rendered frames to be greater than or equal to total frames" + ); + + // check if the rendered frames are decreasing and increasing + let mut prev_frame = player.total_frames(); + for frame in &rendered_frames { + assert!( + frame <= &prev_frame, + "Expected frame to be less than or equal to previous frame" + ); + prev_frame = *frame; + } + + for frame in rendered_frames.iter().rev() { + assert!( + frame >= &prev_frame, + "Expected frame to be greater than or equal to previous frame" + ); + prev_frame = *frame; + } + + // check if the last frame is 0 + assert_eq!(0.0, prev_frame, "Expected last frame to be 0"); + } +} diff --git a/dotlottie-rs/tests/speed.rs b/dotlottie-rs/tests/speed.rs new file mode 100644 index 00000000..f888f8bf --- /dev/null +++ b/dotlottie-rs/tests/speed.rs @@ -0,0 +1,148 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_default_speed() { + let player = DotLottiePlayer::new(Config::default()); + + assert_eq!(player.config().speed, 1.0); + } + + #[test] + fn test_set_speed() { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.speed = 2.0; + player.set_config(config); + + assert_eq!(player.config().speed, 2.0); + } + + #[test] + fn test_playback_speed_accuracy() { + let configs: Vec<(Config, f32)> = + vec![ + // test with default config + ( + Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + 1.0, + ), + // test with different speeds + ( + Config { + speed: 2.0, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + 2.0, + ), + ( + Config { + speed: 0.5, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + 0.5, + ), + // test with a segment + ( + Config { + speed: 2.0, + segment: vec![10.0, 30.0], + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + 2.0, + ), + ( + Config { + speed: 0.4, + autoplay: true, + segment: vec![10.0, 30.0], + use_frame_interpolation: false, + ..Config::default() + }, + 0.4, + ), + ]; + + for (config, expected_speed) in configs { + let player = DotLottiePlayer::new(config); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + assert!(player.is_playing(), "Animation should be playing"); + + let expected_duration = if player.config().segment.is_empty() { + player.duration() + } else { + let segment_total_frames = player.config().segment[1] - player.config().segment[0]; + + segment_total_frames / player.total_frames() * player.duration() + }; + + let start_time = std::time::Instant::now(); + + // animation loop + while !player.is_complete() { + let next_frame = player.request_frame(); + if player.set_frame(next_frame) { + player.render(); + } + } + + let end_time = std::time::Instant::now(); + + let actual_duration = end_time.duration_since(start_time).as_secs_f32(); + + let playback_speed = expected_duration / actual_duration; + + // assert if actual playback speed is close to the expected speed +/- 0.1 + assert!( + (playback_speed - expected_speed).abs() <= 0.1, + "Expected playback speed to be close to {}, found {}", + expected_speed, + playback_speed + ); + } + } + + #[test] + fn test_zero_speed() { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.speed = 0.0; + player.set_config(config); + + assert_eq!(player.config().speed, 1.0); + } + + #[test] + fn test_negative_speed() { + let player = DotLottiePlayer::new(Config::default()); + + let mut config = player.config(); + config.speed = -1.0; + player.set_config(config); + + assert_eq!(player.config().speed, 1.0); + } +} diff --git a/dotlottie-rs/tests/stop.rs b/dotlottie-rs/tests/stop.rs new file mode 100644 index 00000000..1aac947d --- /dev/null +++ b/dotlottie-rs/tests/stop.rs @@ -0,0 +1,115 @@ +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +use dotlottie_player_core::{Config, DotLottiePlayer, Mode}; + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_stop() { + let configs: Vec = vec![ + Config { + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::Reverse, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::Bounce, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::ReverseBounce, + autoplay: true, + use_frame_interpolation: false, + ..Config::default() + }, + // test with different segments + Config { + autoplay: true, + segment: vec![10.0, 30.0], + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::Reverse, + autoplay: true, + segment: vec![10.0, 30.0], + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::Bounce, + autoplay: true, + segment: vec![10.0, 30.0], + use_frame_interpolation: false, + ..Config::default() + }, + Config { + mode: Mode::ReverseBounce, + autoplay: true, + segment: vec![10.0, 30.0], + use_frame_interpolation: false, + ..Config::default() + }, + ]; + + for config in configs { + let player = DotLottiePlayer::new(config); + + assert!( + player.load_animation_path("tests/assets/test.json", WIDTH, HEIGHT), + "Animation should load" + ); + + assert!(player.is_playing(), "Animation should be playing"); + + let (start_frame, end_frame) = if player.config().segment.is_empty() { + (0.0, player.total_frames()) + } else { + (player.config().segment[0], player.config().segment[1]) + }; + + // wait until we're half way to the end + let mid_frame = (start_frame + end_frame) / 2.0; + + assert!(player.set_frame(mid_frame), "Frame should be set"); + assert!(player.render(), "Frame should render"); + assert!(player.is_playing(), "Animation should be playing"); + + assert!(player.stop(), "Animation should stop"); + + assert!(!player.is_playing(), "Animation should not be playing"); + assert!(player.is_stopped(), "Animation should be stopped"); + assert!(!player.is_paused(), "Animation should not be paused"); + + // based on the mode the current frame should be at the start or end + match player.config().mode { + Mode::Forward => { + assert_eq!(player.current_frame(), start_frame); + } + Mode::Reverse => { + assert_eq!(player.current_frame(), end_frame); + } + Mode::Bounce => { + assert_eq!(player.current_frame(), start_frame); + } + Mode::ReverseBounce => { + assert_eq!(player.current_frame(), end_frame); + } + } + + assert!(!player.stop(), "Animation should not stop again"); + } + } +} diff --git a/dotlottie-rs/tests/test_utils.rs b/dotlottie-rs/tests/test_utils.rs new file mode 100644 index 00000000..70528477 --- /dev/null +++ b/dotlottie-rs/tests/test_utils.rs @@ -0,0 +1,2 @@ +pub const WIDTH: u32 = 100; +pub const HEIGHT: u32 = 100; diff --git a/dotlottie-rs/tests/theming.rs b/dotlottie-rs/tests/theming.rs new file mode 100644 index 00000000..5cbc1e80 --- /dev/null +++ b/dotlottie-rs/tests/theming.rs @@ -0,0 +1,82 @@ +use dotlottie_player_core::{Config, DotLottiePlayer}; + +mod test_utils; +use crate::test_utils::{HEIGHT, WIDTH}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_valid_theme() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + ..Config::default() + }); + + let valid_theme_id = "test_theme"; + + assert!( + !player.load_theme(valid_theme_id), + "Expected theme to not load" + ); + + assert!(player.load_dotlottie_data(include_bytes!("assets/test.lottie"), WIDTH, HEIGHT)); + + assert!(player.load_theme(valid_theme_id), "Expected theme to load"); + + assert!(player.is_playing()); + } + + #[test] + fn test_load_invalid_theme() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + ..Config::default() + }); + + let invalid_theme_id = "invalid_theme"; + + assert!( + !player.load_theme(invalid_theme_id), + "Expected theme to not load" + ); + + assert!(player.load_dotlottie_data(include_bytes!("assets/test.lottie"), WIDTH, HEIGHT)); + + assert!( + !player.load_theme(invalid_theme_id), + "Expected theme to not load" + ); + + assert!(player.is_playing()); + } + + #[test] + #[ignore = "malloc: Double free detected when unloading theme"] + fn test_unload_theme() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + ..Config::default() + }); + + let theme_id = "test_theme"; + + assert!(player.load_dotlottie_data(include_bytes!("assets/test.lottie"), WIDTH, HEIGHT)); + + assert!(player.load_theme(theme_id), "Expected theme to load"); + assert!(player.load_theme(""), "Expected theme to unload"); + } + + #[test] + fn test_unload_theme_before_load() { + let player = DotLottiePlayer::new(Config { + autoplay: true, + ..Config::default() + }); + + assert!(player.load_dotlottie_data(include_bytes!("assets/test.lottie"), WIDTH, HEIGHT)); + + assert!(player.load_theme(""), "Expected theme to unload"); + } +}