1use std::{ffi::CString, fmt, path::Path};
18
19use crate::{interop, prelude::*, Canvas, FontMgr, Rect, Size};
20use skia_bindings::{self as sb, SkNVRefCnt};
21
22use super::resources::NativeResourceProvider;
23
24pub type Animation = RCHandle<sb::skottie_Animation>;
28unsafe_send_sync!(Animation);
29require_base_type!(sb::skottie_Animation, SkNVRefCnt);
30
31impl NativeRefCounted for sb::skottie_Animation {
32 fn _ref(&self) {
33 unsafe { sb::C_skottie_Animation_ref(self) }
34 }
35
36 fn _unref(&self) {
37 unsafe { sb::C_skottie_Animation_unref(self) }
38 }
39
40 fn unique(&self) -> bool {
41 unsafe { sb::C_skottie_Animation_unique(self) }
42 }
43}
44
45impl fmt::Debug for Animation {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.debug_struct("Animation")
48 .field("version", &self.version())
49 .field("duration", &self.duration())
50 .field("fps", &self.fps())
51 .field("size", &self.size())
52 .finish()
53 }
54}
55
56bitflags::bitflags! {
57 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59 pub struct RenderFlags: u32 {
60 const SKIP_TOP_LEVEL_ISOLATION = 0x01;
63 const DISABLE_TOP_LEVEL_CLIPPING = 0x02;
65 }
66}
67
68bitflags::bitflags! {
69 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
71 pub struct BuilderFlags: u32 {
72 const DEFER_IMAGE_LOADING = 0x01;
74 const PREFER_EMBEDDED_FONTS = 0x02;
76 }
77}
78
79pub type Builder = RefHandle<sb::skottie_Animation_Builder>;
93
94impl NativeDrop for sb::skottie_Animation_Builder {
95 fn drop(&mut self) {
96 unsafe { sb::C_skottie_Builder_delete(self) }
97 }
98}
99
100impl Default for Builder {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl fmt::Debug for Builder {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 f.debug_struct("Builder").finish()
109 }
110}
111
112impl Builder {
113 pub fn new() -> Self {
115 Self::from_ptr(unsafe { sb::C_skottie_Builder_new(0) }).unwrap()
116 }
117
118 pub fn with_flags(flags: BuilderFlags) -> Self {
120 Self::from_ptr(unsafe { sb::C_skottie_Builder_new(flags.bits()) }).unwrap()
121 }
122
123 pub fn set_font_manager(mut self, font_mgr: FontMgr) -> Self {
127 unsafe { sb::C_skottie_Builder_setFontManager(self.native_mut(), font_mgr.into_ptr()) }
128 self
129 }
130
131 pub fn set_resource_provider(mut self, provider: impl Into<NativeResourceProvider>) -> Self {
138 let provider = provider.into();
139 unsafe { sb::C_skottie_Builder_setResourceProvider(self.native_mut(), provider.into_ptr()) }
140 self
141 }
142
143 pub fn make(mut self, json: impl AsRef<str>) -> Option<Animation> {
147 let json = json.as_ref();
148 Animation::from_ptr(unsafe {
149 sb::C_skottie_Builder_make(self.native_mut(), json.as_ptr() as _, json.len())
150 })
151 }
152
153 pub fn make_from_file(mut self, path: impl AsRef<Path>) -> Option<Animation> {
158 let path = CString::new(path.as_ref().to_str()?).ok()?;
159 Animation::from_ptr(unsafe {
160 sb::C_skottie_Builder_makeFromFile(self.native_mut(), path.as_ptr())
161 })
162 }
163}
164
165impl Animation {
166 #[allow(clippy::should_implement_trait)]
170 pub fn from_str(json: impl AsRef<str>) -> Option<Self> {
171 Self::from_bytes(json.as_ref().as_bytes())
172 }
173
174 pub fn from_bytes(json: &[u8]) -> Option<Self> {
178 Self::from_ptr(unsafe {
179 sb::C_skottie_Animation_Make(json.as_ptr() as *const _, json.len())
180 })
181 }
182
183 pub fn from_file(path: impl AsRef<Path>) -> Option<Self> {
188 let path_str = path.as_ref().to_str()?;
189 let c_path = CString::new(path_str).ok()?;
190 Self::from_ptr(unsafe { sb::C_skottie_Animation_MakeFromFile(c_path.as_ptr()) })
191 }
192
193 pub fn version(&self) -> interop::String {
195 let mut version = interop::String::default();
196 unsafe { sb::C_skottie_Animation_version(self.native(), version.native_mut()) };
197 version
198 }
199
200 pub fn duration(&self) -> f32 {
202 unsafe { sb::C_skottie_Animation_duration(self.native()) }
203 }
204
205 pub fn fps(&self) -> f32 {
207 unsafe { sb::C_skottie_Animation_fps(self.native()) }
208 }
209
210 pub fn in_point(&self) -> f32 {
212 unsafe { sb::C_skottie_Animation_inPoint(self.native()) }
213 }
214
215 pub fn out_point(&self) -> f32 {
217 unsafe { sb::C_skottie_Animation_outPoint(self.native()) }
218 }
219
220 pub fn size(&self) -> Size {
222 let mut size = Size::default();
223 unsafe { sb::C_skottie_Animation_size(self.native(), size.native_mut()) };
224 size
225 }
226
227 pub fn seek(&self, t: f32) {
234 unsafe { sb::C_skottie_Animation_seek(self.native() as *const _ as *mut _, t) }
235 }
236
237 pub fn seek_frame(&self, frame: f64) {
245 unsafe { sb::C_skottie_Animation_seekFrame(self.native() as *const _ as *mut _, frame) }
246 }
247
248 pub fn seek_frame_time(&self, time: f64) {
255 unsafe { sb::C_skottie_Animation_seekFrameTime(self.native() as *const _ as *mut _, time) }
256 }
257
258 pub fn render(&self, canvas: &Canvas, dst: impl Into<Option<Rect>>) {
263 let dst = dst.into();
264 unsafe {
265 sb::C_skottie_Animation_render(
266 self.native(),
267 canvas.native_mut(),
268 dst.as_ref()
269 .map(|r| r.native() as *const _)
270 .unwrap_or(std::ptr::null()),
271 )
272 }
273 }
274
275 pub fn render_with_flags(
280 &self,
281 canvas: &Canvas,
282 dst: impl Into<Option<Rect>>,
283 flags: RenderFlags,
284 ) {
285 let dst = dst.into();
286 unsafe {
287 sb::C_skottie_Animation_render_with_flags(
288 self.native(),
289 canvas.native_mut(),
290 dst.as_ref()
291 .map(|r| r.native() as *const _)
292 .unwrap_or(std::ptr::null()),
293 flags.bits(),
294 )
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::surfaces;
303
304 #[test]
305 fn parse_minimal_animation() {
306 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
307 let anim = Animation::from_str(json).expect("Failed to parse animation");
308
309 assert_eq!(anim.version().as_str(), "5.5.7");
310 assert_eq!(anim.fps(), 30.0);
311 assert_eq!(anim.in_point(), 0.0);
312 assert_eq!(anim.out_point(), 60.0);
313 assert!((anim.duration() - 2.0).abs() < 0.001);
315 assert_eq!(anim.size(), Size::new(200.0, 200.0));
316 }
317
318 #[test]
319 fn render_animation() {
320 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
321 let anim = Animation::from_str(json).expect("Failed to parse animation");
322
323 let mut surface = surfaces::raster_n32_premul((200, 200)).unwrap();
324 anim.seek(0.0);
325 anim.render(surface.canvas(), None);
326 }
327
328 #[test]
329 fn invalid_json_returns_none() {
330 assert!(Animation::from_str("not valid json").is_none());
331 assert!(Animation::from_str("{}").is_none());
332 }
333
334 #[test]
335 fn builder_basic() {
336 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
337 let anim = Builder::new().make(json).expect("build failed");
338 assert_eq!(anim.fps(), 30.0);
339 }
340
341 #[test]
342 fn builder_with_flags() {
343 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
344 let anim = Builder::with_flags(BuilderFlags::DEFER_IMAGE_LOADING)
345 .make(json)
346 .expect("build failed");
347 assert_eq!(anim.fps(), 30.0);
348 }
349
350 #[test]
351 fn builder_with_font_manager() {
352 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
353 let font_mgr = FontMgr::default();
354 let anim = Builder::new()
355 .set_font_manager(font_mgr)
356 .make(json)
357 .expect("build failed");
358 assert_eq!(anim.fps(), 30.0);
359 }
360
361 #[test]
362 fn builder_default() {
363 let builder: Builder = Default::default();
365 let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
366 let _anim = builder.make(json).expect("build failed");
367 }
368}