Skip to main content

rustmotion_components/
image.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use skia_safe::{Canvas, Paint, Rect};
4
5use rustmotion_core::css::CssStyle;
6use rustmotion_core::engine::animator::AnimatedProperties;
7use rustmotion_core::engine::layout_pass::BoxLayout;
8use rustmotion_core::engine::renderer::asset_cache;
9use rustmotion_core::schema::{ImageFit, TimelineStep};
10use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};
11
12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
13pub struct Image {
14    pub src: String,
15    #[serde(default)]
16    pub fit: ImageFit,
17    #[serde(flatten)]
18    pub timing: TimingConfig,
19    #[serde(default)]
20    pub style: CssStyle,
21    #[serde(default)]
22    pub timeline: Vec<TimelineStep>,
23    #[serde(default)]
24    pub stagger: Option<f32>,
25}
26
27rustmotion_core::impl_traits!(Image {
28    Animatable => animation,
29    Timed => timing,
30    Styled => style,
31});
32
33impl Painter for Image {
34    fn paint_content(
35        &self,
36        canvas: &Canvas,
37        layout: &BoxLayout,
38        _props: &AnimatedProperties,
39        _ctx: &PaintCtx,
40    ) {
41        let cache = asset_cache();
42        let img = if let Some(cached) = cache.get(&self.src) {
43            cached.clone()
44        } else {
45            let Ok(data) = std::fs::read(&self.src) else {
46                return;
47            };
48            let skia_data = skia_safe::Data::new_copy(&data);
49            let Some(decoded) = skia_safe::Image::from_encoded(skia_data) else {
50                return;
51            };
52            cache.insert(self.src.clone(), decoded.clone());
53            decoded
54        };
55
56        let img_w = img.width() as f32;
57        let img_h = img.height() as f32;
58        let target_w = layout.width;
59        let target_h = layout.height;
60
61        let (draw_w, draw_h, offset_x, offset_y) = match self.fit {
62            ImageFit::Fill => (target_w, target_h, 0.0, 0.0),
63            ImageFit::Contain => {
64                let scale = (target_w / img_w).min(target_h / img_h);
65                let w = img_w * scale;
66                let h = img_h * scale;
67                (w, h, (target_w - w) / 2.0, (target_h - h) / 2.0)
68            }
69            ImageFit::Cover => {
70                let scale = (target_w / img_w).max(target_h / img_h);
71                let w = img_w * scale;
72                let h = img_h * scale;
73                (w, h, (target_w - w) / 2.0, (target_h - h) / 2.0)
74            }
75        };
76
77        let dst = Rect::from_xywh(offset_x, offset_y, draw_w, draw_h);
78        let paint = Paint::default();
79
80        if matches!(self.fit, ImageFit::Cover) {
81            canvas.save();
82            canvas.clip_rect(
83                Rect::from_xywh(0.0, 0.0, target_w, target_h),
84                skia_safe::ClipOp::Intersect,
85                true,
86            );
87            canvas.draw_image_rect(img, None, dst, &paint);
88            canvas.restore();
89        } else {
90            canvas.draw_image_rect(img, None, dst, &paint);
91        }
92    }
93}