use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::{Canvas, ColorType, ImageInfo, Paint, Rect};
use crate::engine::renderer::asset_cache;
use crate::layout::{Constraints, LayoutNode};
use crate::schema::{LayerStyle, Size};
use crate::traits::{RenderContext, TimingConfig, Widget};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Lottie {
#[serde(default)]
pub src: Option<String>,
#[serde(default)]
pub data: Option<String>,
#[serde(default)]
pub size: Option<Size>,
#[serde(default = "default_speed")]
pub speed: f32,
#[serde(default = "default_true")]
#[serde(rename = "loop")]
pub repeat: bool,
#[serde(default)]
pub frames_dir: Option<String>,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: LayerStyle,
}
fn default_speed() -> f32 {
1.0
}
fn default_true() -> bool {
true
}
crate::impl_traits!(Lottie {
Animatable => style,
Timed => timing,
Styled => style,
});
impl Lottie {
fn parse_metadata(&self) -> Result<(f64, usize, f64, f32, f32)> {
let json_str = if let Some(ref src) = self.src {
std::fs::read_to_string(src)
.map_err(|e| anyhow::anyhow!("Failed to read Lottie file '{}': {}", src, e))?
} else if let Some(ref data) = self.data {
data.clone()
} else {
return Err(anyhow::anyhow!("Lottie component requires either 'src' or 'data'"));
};
let json: serde_json::Value = serde_json::from_str(&json_str)?;
let fr = json["fr"].as_f64().unwrap_or(30.0);
let ip = json["ip"].as_f64().unwrap_or(0.0);
let op = json["op"].as_f64().unwrap_or(60.0);
let w = json["w"].as_f64().unwrap_or(200.0) as f32;
let h = json["h"].as_f64().unwrap_or(200.0) as f32;
let total_frames = (op - ip) as usize;
let duration = total_frames as f64 / fr;
Ok((fr, total_frames, duration, w, h))
}
fn cache_key(&self, frame: usize) -> String {
let src = self.src.as_deref().unwrap_or("inline");
format!("lottie:{}:frame:{}", src, frame)
}
fn load_frame_from_dir(&self, frames_dir: &str, frame: usize) -> Result<skia_safe::Image> {
let frame_path = format!("{}/{:04}.png", frames_dir, frame);
let data = std::fs::read(&frame_path)
.map_err(|e| anyhow::anyhow!("Failed to read frame '{}': {}", frame_path, e))?;
let img = image::load_from_memory(&data)
.map_err(|e| anyhow::anyhow!("Failed to decode frame '{}': {}", frame_path, e))?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
let img_data = skia_safe::Data::new_copy(rgba.as_raw());
let img_info = ImageInfo::new(
(w as i32, h as i32),
ColorType::RGBA8888,
skia_safe::AlphaType::Unpremul,
None,
);
skia_safe::images::raster_from_data(&img_info, img_data, w as usize * 4)
.ok_or_else(|| anyhow::anyhow!("Failed to create Skia image from frame"))
}
}
impl Widget for Lottie {
fn render(
&self,
canvas: &Canvas,
layout: &LayoutNode,
ctx: &RenderContext,
_props: &crate::engine::animator::AnimatedProperties,
) -> Result<()> {
let (fr, total_frames, duration, _intrinsic_w, _intrinsic_h) = self.parse_metadata()?;
if total_frames == 0 {
return Ok(());
}
let anim_time = ctx.time * self.speed as f64;
let effective_time = if self.repeat && duration > 0.0 {
anim_time % duration
} else {
anim_time.min(duration)
};
let frame = ((effective_time * fr) as usize).min(total_frames.saturating_sub(1));
let cache_key = self.cache_key(frame);
let cache = asset_cache();
let img = if let Some(cached) = cache.get(&cache_key) {
cached.clone()
} else if let Some(ref frames_dir) = self.frames_dir {
let img = self.load_frame_from_dir(frames_dir, frame)?;
cache.insert(cache_key, img.clone());
img
} else {
return Err(anyhow::anyhow!(
"Lottie component requires 'frames_dir' pointing to pre-rendered PNG frames.\n\
You can generate frames using: npx lottie-to-frames <lottie.json> --output <dir>\n\
Or use puppeteer/lottie-web to render frames."
));
};
let dst = Rect::from_xywh(0.0, 0.0, layout.width, layout.height);
let paint = Paint::default();
canvas.draw_image_rect(img, None, dst, &paint);
Ok(())
}
fn measure(&self, _constraints: &Constraints) -> (f32, f32) {
if let Some(ref size) = self.size {
return (size.width, size.height);
}
if let Ok((_, _, _, w, h)) = self.parse_metadata() {
return (w, h);
}
(200.0, 200.0)
}
}