skia_safe/modules/
skottie.rs

1//! Skottie - Lottie Animation Support
2//!
3//! This module provides support for rendering Lottie animations via Skia's Skottie module.
4//!
5//! # Example
6//!
7//! ```no_run
8//! use skia_safe::{skottie::Animation, surfaces};
9//!
10//! let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
11//! if let Some(animation) = Animation::from_str(json) {
12//!     let mut surface = surfaces::raster_n32_premul((200, 200)).unwrap();
13//!     animation.render(surface.canvas(), None);
14//! }
15//! ```
16
17use 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
24/// A Lottie animation that can be rendered to a canvas.
25///
26/// Animations are reference-counted and can be cloned cheaply.
27pub 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    /// Flags for rendering control.
58    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59    pub struct RenderFlags: u32 {
60        /// When rendering into a transparent canvas, disables the implicit
61        /// top-level isolation layer.
62        const SKIP_TOP_LEVEL_ISOLATION = 0x01;
63        /// Disables the top-level clipping to the animation bounds.
64        const DISABLE_TOP_LEVEL_CLIPPING = 0x02;
65    }
66}
67
68bitflags::bitflags! {
69    /// Flags for configuring the animation builder.
70    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
71    pub struct BuilderFlags: u32 {
72        /// Defer image loading until the image asset is actually used.
73        const DEFER_IMAGE_LOADING = 0x01;
74        /// Prefer embedded fonts over system fonts.
75        const PREFER_EMBEDDED_FONTS = 0x02;
76    }
77}
78
79/// A builder for creating [`Animation`] instances with custom configuration.
80///
81/// The builder allows setting a resource provider for loading external assets,
82/// a font manager for text rendering, and various flags to control animation loading.
83///
84/// # Example
85///
86/// ```no_run
87/// use skia_safe::skottie::Builder;
88///
89/// let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
90/// let animation = Builder::new().make(json);
91/// ```
92pub 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    /// Create a new animation builder with default settings.
114    pub fn new() -> Self {
115        Self::from_ptr(unsafe { sb::C_skottie_Builder_new(0) }).unwrap()
116    }
117
118    /// Create a new animation builder with the specified flags.
119    pub fn with_flags(flags: BuilderFlags) -> Self {
120        Self::from_ptr(unsafe { sb::C_skottie_Builder_new(flags.bits()) }).unwrap()
121    }
122
123    /// Set the font manager to use for text rendering.
124    ///
125    /// Consumes and returns self for method chaining.
126    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    /// Set the resource provider for loading external assets.
132    ///
133    /// The resource provider is used to load images, fonts, and other external
134    /// resources referenced by the animation.
135    ///
136    /// Consumes and returns self for method chaining.
137    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    /// Build an animation from a JSON string.
144    ///
145    /// Returns `None` if the JSON cannot be parsed as a valid Lottie animation.
146    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    /// Build an animation from a file path.
154    ///
155    /// Returns `None` if the file cannot be loaded or parsed.
156    /// Note: This will return `None` for non-UTF8 paths or paths containing null bytes.
157    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    /// Parse a Lottie animation from a JSON string.
167    ///
168    /// Returns `None` if the string cannot be parsed.
169    #[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    /// Parse a Lottie animation from JSON bytes.
175    ///
176    /// Returns `None` if the data cannot be parsed.
177    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    /// Load a Lottie animation from a file path.
184    ///
185    /// Returns `None` if the file cannot be loaded or parsed.
186    /// Note: This will return `None` for non-UTF8 paths or paths containing null bytes.
187    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    /// Returns the Lottie format version string from the animation file.
194    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    /// Returns the animation duration in seconds.
201    pub fn duration(&self) -> f32 {
202        unsafe { sb::C_skottie_Animation_duration(self.native()) }
203    }
204
205    /// Returns the animation frame rate (frames per second).
206    pub fn fps(&self) -> f32 {
207        unsafe { sb::C_skottie_Animation_fps(self.native()) }
208    }
209
210    /// Returns the first frame index (usually 0).
211    pub fn in_point(&self) -> f32 {
212        unsafe { sb::C_skottie_Animation_inPoint(self.native()) }
213    }
214
215    /// Returns the last frame index.
216    pub fn out_point(&self) -> f32 {
217        unsafe { sb::C_skottie_Animation_outPoint(self.native()) }
218    }
219
220    /// Returns the intrinsic animation size.
221    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    /// Seek to a normalized position in the animation.
228    ///
229    /// `t` is in the range `[0, 1]` where 0 is the first frame and 1 is the last frame.
230    ///
231    /// Note: This method uses interior mutability as the underlying Skia animation
232    /// state is updated internally.
233    pub fn seek(&self, t: f32) {
234        unsafe { sb::C_skottie_Animation_seek(self.native() as *const _ as *mut _, t) }
235    }
236
237    /// Seek to a specific frame number.
238    ///
239    /// Frame numbers are in the range `[in_point(), out_point()]`.
240    /// Fractional frame values are supported for sub-frame accuracy.
241    ///
242    /// Note: This method uses interior mutability as the underlying Skia animation
243    /// state is updated internally.
244    pub fn seek_frame(&self, frame: f64) {
245        unsafe { sb::C_skottie_Animation_seekFrame(self.native() as *const _ as *mut _, frame) }
246    }
247
248    /// Seek to a specific time in seconds.
249    ///
250    /// Time is in the range `[0, duration()]`.
251    ///
252    /// Note: This method uses interior mutability as the underlying Skia animation
253    /// state is updated internally.
254    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    /// Render the current frame to the canvas.
259    ///
260    /// If `dst` is provided, the animation is scaled/positioned to fit the rectangle.
261    /// If `dst` is `None`, the animation is rendered at its intrinsic size at the origin.
262    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    /// Render the current frame to the canvas with additional flags.
276    ///
277    /// If `dst` is provided, the animation is scaled/positioned to fit the rectangle.
278    /// If `dst` is `None`, the animation is rendered at its intrinsic size at the origin.
279    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        // duration = (out_point - in_point) / fps = 60 / 30 = 2.0
314        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        // Test that Default is implemented
364        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}