rustmotion 0.5.0

A CLI tool that renders motion design videos from JSON scenarios. No browser, no Node.js — just a single Rust binary.
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::error::RustmotionError;
use crate::layout::{Constraints, LayoutNode};
use crate::schema::{LayerStyle, Size};
use crate::traits::{RenderContext, TimingConfig, Widget};

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Svg {
    #[serde(default)]
    pub src: Option<String>,
    #[serde(default)]
    pub data: Option<String>,
    #[serde(default)]
    pub size: Option<Size>,
    #[serde(flatten)]
    pub timing: TimingConfig,
    #[serde(default)]
    pub style: LayerStyle,
}

crate::impl_traits!(Svg {
    Animatable => style,
    Timed => timing,
    Styled => style,
});

impl Widget for Svg {
    fn render(&self, canvas: &Canvas, layout: &LayoutNode, _ctx: &RenderContext, _props: &crate::engine::animator::AnimatedProperties) -> Result<()> {
        let (target_w_opt, target_h_opt) = match &self.size {
            Some(size) => (Some(size.width as u32), Some(size.height as u32)),
            None => (None, None),
        };

        let cache_key = if let Some(ref src) = self.src {
            format!("svg:{}:{}x{}", src, target_w_opt.unwrap_or(0), target_h_opt.unwrap_or(0))
        } else if let Some(ref data) = self.data {
            use std::collections::hash_map::DefaultHasher;
            use std::hash::{Hash, Hasher};
            let mut hasher = DefaultHasher::new();
            data.hash(&mut hasher);
            format!("svg-inline:{}:{}x{}", hasher.finish(), target_w_opt.unwrap_or(0), target_h_opt.unwrap_or(0))
        } else {
            return Err(RustmotionError::SvgMissingSrc.into());
        };

        let cache = asset_cache();
        let img = if let Some(cached) = cache.get(&cache_key) {
            cached.clone()
        } else {
            let svg_data = if let Some(ref src) = self.src {
                std::fs::read(src)
                    .map_err(|e| RustmotionError::SvgLoad { path: src.clone(), reason: e.to_string() })?
            } else if let Some(ref data) = self.data {
                data.as_bytes().to_vec()
            } else {
                unreachable!()
            };

            let opt = usvg::Options::default();
            let tree = usvg::Tree::from_data(&svg_data, &opt)
                .map_err(|e| RustmotionError::SvgParse { reason: e.to_string() })?;

            let svg_size = tree.size();
            let target_w = target_w_opt.unwrap_or(svg_size.width() as u32);
            let target_h = target_h_opt.unwrap_or(svg_size.height() as u32);

            let mut pixmap = tiny_skia::Pixmap::new(target_w, target_h)
                .ok_or_else(|| RustmotionError::PixmapCreation { target: "SVG".to_string() })?;

            let scale_x = target_w as f32 / svg_size.width();
            let scale_y = target_h as f32 / svg_size.height();
            let transform = tiny_skia::Transform::from_scale(scale_x, scale_y);

            resvg::render(&tree, transform, &mut pixmap.as_mut());

            let img_data = skia_safe::Data::new_copy(pixmap.data());
            let img_info = ImageInfo::new(
                (target_w as i32, target_h as i32),
                ColorType::RGBA8888,
                skia_safe::AlphaType::Premul,
                None,
            );
            let decoded = skia_safe::images::raster_from_data(&img_info, img_data, target_w as usize * 4)
                .ok_or_else(|| RustmotionError::SkiaImageCreation { target: "SVG".to_string() })?;
            cache.insert(cache_key, decoded.clone());
            decoded
        };

        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) {
        match &self.size {
            Some(s) => (s.width, s.height),
            None => (100.0, 100.0),
        }
    }
}