use std::{ffi::CString, fmt, path::Path};
use crate::{interop, prelude::*, Canvas, FontMgr, Rect, Size};
use skia_bindings::{self as sb, SkNVRefCnt};
use super::resources::NativeResourceProvider;
pub type Animation = RCHandle<sb::skottie_Animation>;
unsafe_send_sync!(Animation);
require_base_type!(sb::skottie_Animation, SkNVRefCnt);
impl NativeRefCounted for sb::skottie_Animation {
fn _ref(&self) {
unsafe { sb::C_skottie_Animation_ref(self) }
}
fn _unref(&self) {
unsafe { sb::C_skottie_Animation_unref(self) }
}
fn unique(&self) -> bool {
unsafe { sb::C_skottie_Animation_unique(self) }
}
}
impl fmt::Debug for Animation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Animation")
.field("version", &self.version())
.field("duration", &self.duration())
.field("fps", &self.fps())
.field("size", &self.size())
.finish()
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RenderFlags: u32 {
const SKIP_TOP_LEVEL_ISOLATION = 0x01;
const DISABLE_TOP_LEVEL_CLIPPING = 0x02;
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct BuilderFlags: u32 {
const DEFER_IMAGE_LOADING = 0x01;
const PREFER_EMBEDDED_FONTS = 0x02;
}
}
pub type Builder = RefHandle<sb::skottie_Animation_Builder>;
impl NativeDrop for sb::skottie_Animation_Builder {
fn drop(&mut self) {
unsafe { sb::C_skottie_Builder_delete(self) }
}
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Builder").finish()
}
}
impl Builder {
pub fn new() -> Self {
Self::from_ptr(unsafe { sb::C_skottie_Builder_new(0) }).unwrap()
}
pub fn with_flags(flags: BuilderFlags) -> Self {
Self::from_ptr(unsafe { sb::C_skottie_Builder_new(flags.bits()) }).unwrap()
}
pub fn set_font_manager(mut self, font_mgr: FontMgr) -> Self {
unsafe { sb::C_skottie_Builder_setFontManager(self.native_mut(), font_mgr.into_ptr()) }
self
}
pub fn set_resource_provider(mut self, provider: impl Into<NativeResourceProvider>) -> Self {
let provider = provider.into();
unsafe { sb::C_skottie_Builder_setResourceProvider(self.native_mut(), provider.into_ptr()) }
self
}
pub fn make(mut self, json: impl AsRef<str>) -> Option<Animation> {
let json = json.as_ref();
Animation::from_ptr(unsafe {
sb::C_skottie_Builder_make(self.native_mut(), json.as_ptr() as _, json.len())
})
}
pub fn make_from_file(mut self, path: impl AsRef<Path>) -> Option<Animation> {
let path = CString::new(path.as_ref().to_str()?).ok()?;
Animation::from_ptr(unsafe {
sb::C_skottie_Builder_makeFromFile(self.native_mut(), path.as_ptr())
})
}
}
impl Animation {
#[allow(clippy::should_implement_trait)]
pub fn from_str(json: impl AsRef<str>) -> Option<Self> {
Self::from_bytes(json.as_ref().as_bytes())
}
pub fn from_bytes(json: &[u8]) -> Option<Self> {
Self::from_ptr(unsafe {
sb::C_skottie_Animation_Make(json.as_ptr() as *const _, json.len())
})
}
pub fn from_file(path: impl AsRef<Path>) -> Option<Self> {
let path_str = path.as_ref().to_str()?;
let c_path = CString::new(path_str).ok()?;
Self::from_ptr(unsafe { sb::C_skottie_Animation_MakeFromFile(c_path.as_ptr()) })
}
pub fn version(&self) -> interop::String {
let mut version = interop::String::default();
unsafe { sb::C_skottie_Animation_version(self.native(), version.native_mut()) };
version
}
pub fn duration(&self) -> f32 {
unsafe { sb::C_skottie_Animation_duration(self.native()) }
}
pub fn fps(&self) -> f32 {
unsafe { sb::C_skottie_Animation_fps(self.native()) }
}
pub fn in_point(&self) -> f32 {
unsafe { sb::C_skottie_Animation_inPoint(self.native()) }
}
pub fn out_point(&self) -> f32 {
unsafe { sb::C_skottie_Animation_outPoint(self.native()) }
}
pub fn size(&self) -> Size {
let mut size = Size::default();
unsafe { sb::C_skottie_Animation_size(self.native(), size.native_mut()) };
size
}
pub fn seek(&self, t: f32) {
unsafe { sb::C_skottie_Animation_seek(self.native() as *const _ as *mut _, t) }
}
pub fn seek_frame(&self, frame: f64) {
unsafe { sb::C_skottie_Animation_seekFrame(self.native() as *const _ as *mut _, frame) }
}
pub fn seek_frame_time(&self, time: f64) {
unsafe { sb::C_skottie_Animation_seekFrameTime(self.native() as *const _ as *mut _, time) }
}
pub fn render(&self, canvas: &Canvas, dst: impl Into<Option<Rect>>) {
let dst = dst.into();
unsafe {
sb::C_skottie_Animation_render(
self.native(),
canvas.native_mut(),
dst.as_ref()
.map(|r| r.native() as *const _)
.unwrap_or(std::ptr::null()),
)
}
}
pub fn render_with_flags(
&self,
canvas: &Canvas,
dst: impl Into<Option<Rect>>,
flags: RenderFlags,
) {
let dst = dst.into();
unsafe {
sb::C_skottie_Animation_render_with_flags(
self.native(),
canvas.native_mut(),
dst.as_ref()
.map(|r| r.native() as *const _)
.unwrap_or(std::ptr::null()),
flags.bits(),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::surfaces;
#[test]
fn parse_minimal_animation() {
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let anim = Animation::from_str(json).expect("Failed to parse animation");
assert_eq!(anim.version().as_str(), "5.5.7");
assert_eq!(anim.fps(), 30.0);
assert_eq!(anim.in_point(), 0.0);
assert_eq!(anim.out_point(), 60.0);
assert!((anim.duration() - 2.0).abs() < 0.001);
assert_eq!(anim.size(), Size::new(200.0, 200.0));
}
#[test]
fn render_animation() {
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let anim = Animation::from_str(json).expect("Failed to parse animation");
let mut surface = surfaces::raster_n32_premul((200, 200)).unwrap();
anim.seek(0.0);
anim.render(surface.canvas(), None);
}
#[test]
fn invalid_json_returns_none() {
assert!(Animation::from_str("not valid json").is_none());
assert!(Animation::from_str("{}").is_none());
}
#[test]
fn builder_basic() {
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let anim = Builder::new().make(json).expect("build failed");
assert_eq!(anim.fps(), 30.0);
}
#[test]
fn builder_with_flags() {
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let anim = Builder::with_flags(BuilderFlags::DEFER_IMAGE_LOADING)
.make(json)
.expect("build failed");
assert_eq!(anim.fps(), 30.0);
}
#[test]
fn builder_with_font_manager() {
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let font_mgr = FontMgr::default();
let anim = Builder::new()
.set_font_manager(font_mgr)
.make(json)
.expect("build failed");
assert_eq!(anim.fps(), 30.0);
}
#[test]
fn builder_default() {
let builder: Builder = Default::default();
let json = r#"{"v":"5.5.7","fr":30,"ip":0,"op":60,"w":200,"h":200,"layers":[]}"#;
let _anim = builder.make(json).expect("build failed");
}
}